├── pnpm-workspace.yaml ├── packages ├── starlight-digital-garden │ ├── .npmignore │ ├── tsconfig.json │ ├── overrides │ │ ├── TableOfContents.astro │ │ └── PageFrame.astro │ ├── components │ │ ├── custom.css │ │ ├── LinkPreview.astro │ │ ├── Backlinks.astro │ │ ├── svgpanzoom.css │ │ ├── svgpanzoom.ts │ │ └── preview.ts │ ├── README.md │ ├── package.json │ └── index.ts ├── website │ ├── src │ │ ├── env.d.ts │ │ ├── components │ │ │ ├── GraphPage.astro │ │ │ ├── TableOfContents.astro │ │ │ ├── permutations.ts │ │ │ ├── EdeapTagsDiagram.astro │ │ │ ├── EdeapDiagram.astro │ │ │ ├── EulerTagsDiagram.astro │ │ │ ├── EulerDiagram.astro │ │ │ ├── TOCHeading.astro │ │ │ ├── Head.astro │ │ │ ├── Tags.astro │ │ │ ├── TOC.astro │ │ │ ├── TagList.astro │ │ │ ├── RecentChanges.astro │ │ │ ├── Alphabetical.astro │ │ │ ├── Graph.astro │ │ │ ├── GraphClient.astro │ │ │ ├── SidebarMulti.astro │ │ │ ├── graphRenderer.ts │ │ │ └── toc.js │ │ ├── pages │ │ │ ├── graph.json.ts │ │ │ └── og │ │ │ │ └── [...slug].ts │ │ ├── content │ │ │ ├── docs │ │ │ │ ├── alphabetical.mdx │ │ │ │ ├── recent.mdx │ │ │ │ ├── recipes │ │ │ │ │ ├── collapsible-sections.md │ │ │ │ │ ├── lazy-embeded-videos.md │ │ │ │ │ ├── diagrams.md │ │ │ │ │ ├── syntax-highlighting.md │ │ │ │ │ ├── detect-broken-links.mdx │ │ │ │ │ ├── math-support-in-markdown.mdx │ │ │ │ │ ├── sortable-tables.md │ │ │ │ │ ├── gfm-aside.md │ │ │ │ │ ├── rehype-plugins-for-code.md │ │ │ │ │ ├── social-images-autogenration.md │ │ │ │ │ ├── search-for-static-website.md │ │ │ │ │ ├── recently-changed-pages.mdx │ │ │ │ │ ├── alphabetical-index.mdx │ │ │ │ │ ├── table-of-contents.mdx │ │ │ │ │ ├── icons.mdx │ │ │ │ │ ├── permalinks.md │ │ │ │ │ ├── content-graph-visualization.md │ │ │ │ │ ├── hasse-diagram.md │ │ │ │ │ ├── backlinks.mdx │ │ │ │ │ ├── braindb.mdx │ │ │ │ │ ├── faceted-search.md │ │ │ │ │ ├── last-modified-time.mdx │ │ │ │ │ ├── wikilinks.mdx │ │ │ │ │ ├── metro-map-diagram.md │ │ │ │ │ ├── icons-to-external-links.mdx │ │ │ │ │ ├── timeline-diagram.md │ │ │ │ │ ├── seo-and-smo-meta-tags.mdx │ │ │ │ │ ├── graphviz-diagram.mdx │ │ │ │ │ ├── anchors-for-headings.mdx │ │ │ │ │ ├── task-extraction.mdx │ │ │ │ │ ├── svg-pan-zoom.mdx │ │ │ │ │ ├── tag-list.mdx │ │ │ │ │ ├── euler-diagram.mdx │ │ │ │ │ ├── obsidian-dataview.mdx │ │ │ │ │ ├── mermaid-diagrams-in-markdown.mdx │ │ │ │ │ ├── gnuplot-diagram.mdx │ │ │ │ │ ├── link-previews.mdx │ │ │ │ │ ├── sidebar.mdx │ │ │ │ │ └── metro-map-diagram │ │ │ │ │ │ └── lisboa.output.svg │ │ │ │ ├── tags.mdx │ │ │ │ ├── graph.mdx │ │ │ │ ├── graph-client.mdx │ │ │ │ ├── tasks.md │ │ │ │ └── index.mdx │ │ │ └── config.ts │ │ ├── plugins │ │ │ └── remark-modified-time.mjs │ │ ├── lib │ │ │ ├── braindb.mjs │ │ │ └── graph.ts │ │ └── styles │ │ │ └── custom.css │ ├── tsconfig.json │ ├── public │ │ └── favicon.svg │ ├── package.json │ └── astro.config.mjs └── starlight-katex │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── src │ └── index.ts ├── .vscode ├── extensions.json └── launch.json ├── .gitignore ├── turbo.json ├── package.json └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /packages/starlight-digital-garden/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /packages/website/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /packages/website/src/components/GraphPage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Graph from "./Graph.astro"; 3 | import { getGraph } from "@lib/graph"; 4 | const graph = await getGraph(); 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/website/src/pages/graph.json.ts: -------------------------------------------------------------------------------- 1 | import { getBrainDb } from "starlight-digital-garden"; 2 | import { toGraphologyJson } from "@lib/graph"; 3 | 4 | export async function GET() { 5 | await getBrainDb().ready(); 6 | return new Response(JSON.stringify(await toGraphologyJson(getBrainDb()))); 7 | } 8 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/alphabetical.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alphabetical index 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: An alphabetical list of all notes on the website 7 | --- 8 | 9 | import Alphabetical from "@components/Alphabetical.astro"; 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recent.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recently changed 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: Notes sorted by modification date to show what has changed recently 7 | --- 8 | 9 | import RecentChanges from "@components/RecentChanges.astro"; 10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "types": ["unplugin-icons/types/astro"], 5 | "baseUrl": ".", 6 | "paths": { 7 | "@components/*": ["src/components/*"], 8 | "@lib/*": ["src/lib/*"] 9 | } 10 | }, 11 | "exclude": ["dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/collapsible-sections.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Collapsible sections 3 | tags: [component] 4 | sidebar: 5 | label: Collapsible sections 🚷 6 | description: How to add a collapsible sections to a static website 7 | --- 8 | 9 | ## Ideas 10 | 11 | - https://orgmode.org/ 12 | - https://roamresearch.com/#/app/help/page/dZ72V0Ig6 13 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/tags.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tags 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: All notes on the website grouped by tags 7 | --- 8 | 9 | import EulerTagsDiagram from "@components/EulerTagsDiagram.astro"; 10 | 11 | 12 | 13 | import TagList from "@components/TagList.astro"; 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | .braindb 24 | .turbo 25 | packages/website/public/beoe/ -------------------------------------------------------------------------------- /packages/website/src/plugins/remark-modified-time.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | 3 | export function remarkModifiedTime() { 4 | return function (_tree, file) { 5 | const filepath = file.history[0]; 6 | const result = execSync(`git log -1 --pretty="format:%cI" "${filepath}"`); 7 | file.data.astro.frontmatter.lastUpdated = result.toString(); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/overrides/TableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro 3 | import Default from "@astrojs/starlight/components/TableOfContents.astro"; 4 | import Backlinks from "../components/Backlinks.astro"; 5 | --- 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "test": {}, 5 | "clean": {}, 6 | "tsc": { 7 | "dependsOn": ["^build"] 8 | }, 9 | "build": { 10 | "dependsOn": ["^build"], 11 | "outputs": ["dist/**"] 12 | }, 13 | "dev": { 14 | "dependsOn": ["^build"], 15 | "cache": false, 16 | "persistent": true 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/custom.css: -------------------------------------------------------------------------------- 1 | .broken-link { 2 | text-decoration: underline; 3 | color: red; 4 | } 5 | 6 | /* rehypeExternalLinks */ 7 | 8 | .no-select { 9 | user-select: none; 10 | } 11 | 12 | .external-icon { 13 | background-image: var(--icon); 14 | background-color: #fff; 15 | background-size: cover; 16 | color: transparent; 17 | padding-left: 1.2rem; 18 | border-radius: 0.2rem; 19 | margin-left: 0.2rem; 20 | } 21 | -------------------------------------------------------------------------------- /packages/website/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { z, defineCollection } from "astro:content"; 2 | import { docsSchema } from "@astrojs/starlight/schema"; 3 | import { docsLoader } from "@astrojs/starlight/loaders"; 4 | 5 | export const collections = { 6 | docs: defineCollection({ 7 | loader: docsLoader(), 8 | schema: docsSchema({ 9 | extend: z.object({ 10 | tags: z.array(z.string()).optional(), 11 | }), 12 | }), 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-digital-garden-root", 3 | "type": "module", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "dependencies": { 9 | "turbo": "^2.4.4", 10 | "typescript": "^5.8.2" 11 | }, 12 | "scripts": { 13 | "test": "turbo run test", 14 | "build": "turbo run build", 15 | "dev": "turbo run dev", 16 | "clean": "turbo run clean", 17 | "tsc": "turbo run tsc" 18 | }, 19 | "packageManager": "pnpm@9.2.0" 20 | } -------------------------------------------------------------------------------- /packages/website/src/lib/braindb.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to differentiate content pages from service pages, like 3 | * alphabetical index, tag list, content graph etc. 4 | * 5 | * @param {typeof require("@braindb/core").Document} doc 6 | * @returns {boolean} 7 | */ 8 | export function isContent(doc) { 9 | // I use tags, but it can be anything 10 | return ( 11 | doc.frontmatter().tags?.length > 0 && 12 | (doc.frontmatter().draft !== true || import.meta.env.DEV) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/overrides/PageFrame.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Default from "@astrojs/starlight/components/PageFrame.astro"; 3 | import LinkPreview from "../components/LinkPreview.astro"; 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /packages/starlight-katex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "strict": true, 7 | "moduleResolution": "Node16", 8 | "target": "ES2022", 9 | "module": "Node16", 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "verbatimModuleSyntax": true, 13 | "stripInternal": true, 14 | "outDir": "./dist" 15 | }, 16 | "include": ["src"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/lazy-embeded-videos.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lazy embeded video 3 | tags: [component] 4 | description: Lazily embed YouTube videos with a static placeholder that looks like the YouTube embed but doesn't actually embed until you click 5 | --- 6 | 7 | ## Astro plugins 8 | 9 | - [astro-lazy-youtube-embed](https://github.com/insin/astro-lazy-youtube-embed) 10 | - [@astro-community/astro-embed-youtube](https://github.com/delucis/astro-embed/tree/main/packages/astro-embed-youtube) 11 | -------------------------------------------------------------------------------- /packages/website/src/components/TableOfContents.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // https://github.com/withastro/starlight/blob/main/packages/starlight/components/TableOfContents.astro 3 | // import Default from "@astrojs/starlight/components/TableOfContents.astro"; 4 | import Backlinks from "starlight-digital-garden/components/Backlinks.astro"; 5 | import Tags from "./Tags.astro"; 6 | import TOC from "./TOC.astro"; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/website/src/components/permutations.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/59942031 2 | export function getCombinations(valuesArray: string[]) { 3 | const combi = []; 4 | let temp = []; 5 | const slent = Math.pow(2, valuesArray.length); 6 | 7 | for (let i = 0; i < slent; i++) { 8 | temp = []; 9 | for (let j = 0; j < valuesArray.length; j++) { 10 | if (i & Math.pow(2, j)) { 11 | temp.push(valuesArray[j]); 12 | } 13 | } 14 | if (temp.length > 0) { 15 | combi.push(temp); 16 | } 17 | } 18 | 19 | combi.sort((a, b) => a.length - b.length); 20 | return combi; 21 | } 22 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/LinkPreview.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /packages/website/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/website/src/components/EdeapTagsDiagram.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from "astro:content"; 3 | import EdeapDiagram from "./EdeapDiagram.astro"; 4 | 5 | const separator = ","; 6 | const docs = await getCollection("docs"); 7 | const docsByTags: Record = {}; 8 | 9 | docs.forEach((doc) => { 10 | if (doc.data.tags && doc.data.tags.length > 0) { 11 | const key = doc.data.tags.sort().join(separator); 12 | docsByTags[key] = (docsByTags[key] || 0) + 1; 13 | } 14 | }); 15 | 16 | const sets = Object.entries(docsByTags).map(([key, size]) => ({ 17 | sets: key.split(separator).map((x) => "#" + x), 18 | size, 19 | })); 20 | --- 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/website/src/components/EdeapDiagram.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCache } from "@beoe/cache"; 3 | import { serialize } from "node:v8"; 4 | import { edeapSvg, type ISetOverlap } from "edeap"; 5 | 6 | interface Props { 7 | items: ISetOverlap[]; 8 | } 9 | 10 | const cache = await getCache(); 11 | const items = Astro.props.items; 12 | const otherParams = { width: 700, height: 350 }; 13 | const cacheKey = serialize({ edeap: items, ...otherParams }); 14 | 15 | let svg = cache.get(cacheKey); 16 | if (!svg) { 17 | svg = edeapSvg({ 18 | ...otherParams, 19 | overlaps: items, 20 | }); 21 | cache.set(cacheKey, svg); 22 | } 23 | --- 24 | 25 |
26 | 27 |
28 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/graph.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Server-side content graph 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: Relationships between notes visualized as a graph, aka network 7 | --- 8 | 9 | :::tip 10 | Please note that this graph is generated on the server side as SVG. 11 | 12 | - **Pros**: It doesn't require a large JS library to run visualization on the client. 13 | - **Cons**: It doesn't look as fancy as graphs generated on the client side and lacks interactivity. 14 | ::: 15 | 16 | import GraphPage from "@components/GraphPage.astro"; 17 | 18 | 19 | 20 | **Features**: 21 | 22 | - Nodes are HTML links. 23 | - Labels are shown when nodes are hovered over (requires JS to work). 24 | - Pan/Zoom works thanks to [[svg-pan-zoom]]. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About digital garden 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) [![Netlify Status](https://api.netlify.com/api/v1/badges/06409150-3184-47d0-b085-6a013b792ed3/deploy-status)](https://app.netlify.com/sites/astro-digital-garden/deploys) 4 | 5 | **Ultimate goal** is create theme and set of tools to publish digital garden with the help of Astro. But for now I collect relevant code snippets and links 6 | 7 | **Examples**: 8 | 9 | - https://astro-digital-garden.stereobooster.com/ 10 | - [Digital garden about fuzzy string matching](https://fuzzy.stereobooster.com/) 11 | - [Digital garden about parsing](https://parsing.stereobooster.com/) 12 | - [Chronology of Exact Online String Matching Algorithms](https://exact.stereobooster.com/) 13 | -------------------------------------------------------------------------------- /packages/website/src/components/EulerTagsDiagram.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from "astro:content"; 3 | import { getCombinations } from "./permutations"; 4 | import EulerDiagram from "./EulerDiagram.astro"; 5 | 6 | const separator = ","; 7 | const docs = await getCollection("docs"); 8 | const docsByTags: Record = {}; 9 | 10 | docs.forEach((doc) => { 11 | if (doc.data.tags && doc.data.tags.length > 0) { 12 | for (const combiination of getCombinations(doc.data.tags)) { 13 | const key = combiination.sort().join(separator); 14 | docsByTags[key] = (docsByTags[key] || 0) + 1; 15 | } 16 | } 17 | }); 18 | 19 | const sets = Object.entries(docsByTags).map(([key, size]) => ({ 20 | sets: key.split(separator).map((x) => "#" + x), 21 | size, 22 | })); 23 | --- 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/website/src/components/EulerDiagram.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { createVennRenderer, type ISetOverlap } from "venn-isomorphic"; 3 | import { getCache } from "@beoe/cache"; 4 | import { serialize } from "node:v8"; 5 | 6 | interface Props { 7 | items: ISetOverlap[]; 8 | } 9 | 10 | const cache = await getCache(); 11 | const items = Astro.props.items; 12 | const cacheKey = serialize({ venn: items }); 13 | 14 | let svg = cache.get(cacheKey); 15 | if (!svg) { 16 | const renderer = createVennRenderer(); 17 | const results = await renderer([items]); 18 | const [result] = results; 19 | if (result.status == "fulfilled") { 20 | svg = result.value.svg; 21 | cache.set(cacheKey, svg); 22 | } else { 23 | throw new Error(result.reason); 24 | } 25 | } 26 | --- 27 | 28 |
29 | 30 |
31 | -------------------------------------------------------------------------------- /packages/website/src/components/TOCHeading.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { MarkdownHeading } from "astro"; 3 | type TOCItem = MarkdownHeading & { 4 | children: TOCItem[]; 5 | }; 6 | export type Props = { headings: TOCItem[] }; 7 | const { headings } = Astro.props; 8 | --- 9 | 10 | { 11 | headings.length > 0 && ( 12 |
    13 | {headings.map((heading) => ( 14 |
  • 15 | {heading.text} 16 | 17 |
  • 18 | ))} 19 |
20 | ) 21 | } 22 | 23 | 37 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/diagrams.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Diagrams 3 | tags: [component, diagram] 4 | description: Overview of available options for diagrams in Markdown and Astro (text-to-diagram approach) 5 | --- 6 | 7 | There are two ways to work with diagrams: 8 | 9 | 1. Embed them in Markdown using code fences. This is the so-called [text-to-diagram](https://stereobooster.com/posts/text-to-diagram/) approach. For example: 10 | - [[mermaid-diagrams-in-markdown|Mermaid]] 11 | - [[gnuplot-diagram]] 12 | - [[graphviz-diagram]] 13 | - [Astro D2](https://beoe.stereobooster.com/diagrams/d2/) 14 | 2. Use them as components inside MDX or Astro pages: 15 | - [[timeline-diagram]] 16 | - [[euler-diagram]] 17 | - [[hasse-diagram]] 18 | - [[content-graph-visualization|Graph]] 19 | - [[metro-map-diagram]] 20 | 21 | ## Ideal solution 22 | 23 | Read more about different options and trade-offs [here](https://beoe.stereobooster.com/start-here/strategy/). 24 | -------------------------------------------------------------------------------- /packages/website/src/components/Head.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Default from "@astrojs/starlight/components/Head.astro"; 3 | 4 | const ogImageUrl = new URL( 5 | `/og/${Astro.locals.starlightRoute.entry.id.replace(/\.\w+$/, ".png")}`, 6 | Astro.site 7 | ); 8 | 9 | import { Schema } from "astro-seo-schema"; 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/website/src/components/Tags.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { CollectionEntry } from "astro:content"; 3 | interface Props { 4 | entry: CollectionEntry<"docs">; 5 | } 6 | const { entry } = Astro.props; 7 | --- 8 | 9 |
10 | { 11 | entry.data.tags && entry.data.tags.length > 0 && ( 12 | <> 13 |

Tags

14 |
    15 | {entry.data.tags.map((tag) => ( 16 |
  • 17 | #{tag} 18 |
  • 19 | ))} 20 |
21 | 22 | ) 23 | } 24 |
25 | 26 | 47 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/syntax-highlighting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Syntax Highlighting 3 | tags: [markdown, code-fences] 4 | description: Different options for syntax highlighting (code fences in Markdown) for Astro-based websites. 5 | --- 6 | 7 | Astro comes with built-in support for Shiki and Prism, providing syntax highlighting for code fences. 8 | 9 | Starlight uses [Expressive Code](https://expressive-code.com/). 10 | 11 | ## Alternatives 12 | 13 | - [Code Hike](https://codehike.org/) 14 | - [codehike#255](https://github.com/code-hike/codehike/issues/255) 15 | - [withastro/discussions#470](https://github.com/withastro/roadmap/discussions/470) 16 | - [Shiki-Twoslash](https://shikijs.github.io/twoslash/) 17 | - [starlight/discussions#1381](https://github.com/withastro/starlight/discussions/1381) 18 | - [Sandpack](https://sandpack.codesandbox.io/) 19 | - [A World-Class Code Playground with Sandpack](https://www.joshwcomeau.com/react/next-level-playground/) 20 | - [Shaku](https://shaku-web.vercel.app/) 21 | -------------------------------------------------------------------------------- /packages/website/src/components/TOC.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TOCHeading from "./TOCHeading.astro"; 3 | const toc = Astro.locals.starlightRoute.toc?.items || []; 4 | --- 5 | 6 |

On this page

7 | 20 | 21 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /packages/website/src/components/TagList.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { isContent } from "@lib/braindb.mjs"; 3 | import { getBrainDb } from "starlight-digital-garden"; 4 | import type { Document } from "@braindb/core"; 5 | 6 | const docsByTags = new Map(); 7 | (await getBrainDb().documents()).forEach((doc) => { 8 | if (isContent(doc)) { 9 | // @ts-expect-error 10 | doc.frontmatter().tags.forEach((tag: string) => { 11 | docsByTags.set(tag, docsByTags.get(tag) || []); 12 | docsByTags.get(tag)?.push(doc); 13 | }); 14 | } 15 | }); 16 | 17 | const comparator = new Intl.Collator("en"); 18 | const tagsSorted = [...docsByTags.keys()].sort(comparator.compare); 19 | --- 20 | 21 |
22 | 23 | { 24 | tagsSorted.map((tag) => ( 25 |

26 | #{tag} 27 |

    28 | {docsByTags.get(tag)?.map((doc) => ( 29 |
  • 30 | {doc.title()} 31 |
  • 32 | ))} 33 |
34 |

35 | )) 36 | } 37 |
38 |
39 | -------------------------------------------------------------------------------- /packages/website/src/components/RecentChanges.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { isContent } from "@lib/braindb.mjs"; 3 | import { getBrainDb } from "starlight-digital-garden"; 4 | import type { Document } from "@braindb/core"; 5 | 6 | const docsByDate = new Map(); 7 | (await getBrainDb().documents({ sort: ["updated_at", "desc"] })).forEach((doc) => { 8 | if (isContent(doc)) { 9 | const date = doc.updatedAt().toISOString().split("T")[0]; 10 | docsByDate.set(date, docsByDate.get(date) || []); 11 | docsByDate.get(date)?.push(doc); 12 | } 13 | }); 14 | 15 | const dates = Array.from(docsByDate.keys()).map((d) => [ 16 | d, 17 | d.split("-").reverse().join("."), 18 | ]); 19 | --- 20 | 21 |
22 | 23 | { 24 | dates.map(([key, title]) => ( 25 |

26 | {title} 27 |

    28 | {docsByDate.get(key)?.map((doc) => ( 29 |
  • 30 | {doc.title()} 31 |
  • 32 | ))} 33 |
34 |

35 | )) 36 | } 37 |
38 |
39 | -------------------------------------------------------------------------------- /packages/starlight-katex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starlight-katex", 3 | "author": "stereobooster", 4 | "version": "0.0.4", 5 | "license": "MIT", 6 | "description": "Starlight plugin for Katex", 7 | "keywords": [ 8 | "starlight", 9 | "plugin", 10 | "katex", 11 | "latex", 12 | "math", 13 | "astro" 14 | ], 15 | "type": "module", 16 | "exports": { 17 | ".": "./src/index.ts" 18 | }, 19 | "scripts": {}, 20 | "devDependencies": { 21 | "@astrojs/starlight": "^0.28.3", 22 | "astro": "^4.16.18" 23 | }, 24 | "peerDependencies": { 25 | "@astrojs/starlight": ">=0.16.0", 26 | "astro": ">=4.0.0" 27 | }, 28 | "engines": { 29 | "node": ">=18" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/stereobooster/astro-digital-garden.git", 34 | "directory": "packages/starlight-katex" 35 | }, 36 | "dependencies": { 37 | "astro-integration-kit": "^0.16.1", 38 | "import-meta-resolve": "^4.1.0", 39 | "katex": "^0.16.21", 40 | "rehype-class-names": "^2.0.0", 41 | "rehype-katex": "^7.0.1", 42 | "remark-math": "^6.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/detect-broken-links.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Detect broken links 3 | tags: [link] 4 | description: A small script to detect internal broken links, as they are bad for UX and SEO 5 | --- 6 | 7 | ## Installation 8 | 9 | import { Steps } from "@astrojs/starlight/components"; 10 | 11 | 12 | 13 | 1. Install [[braindb#installation]] 14 | 15 | 2. Configure BrainDB 16 | 17 | ```js 18 | // src/lib/braindb.mjs 19 | import { getBrainDb } from "@braindb/astro"; 20 | 21 | let unresolvedLinksCount = 0; 22 | getBrainDb().on("*", (action, opts) => { 23 | if (opts) { 24 | opts.document.unresolvedLinks().forEach((link) => { 25 | unresolvedLinksCount++; 26 | console.log( 27 | `Unresolved link: ${link 28 | .from() 29 | .path()}:${link.line()}:${link.column()}` 30 | ); 31 | }); 32 | } 33 | // fail build if there are broken links 34 | if ( 35 | import.meta.env.PROD && 36 | action === "ready" && 37 | unresolvedLinksCount > 0 38 | ) { 39 | process.exit(1); 40 | } 41 | }); 42 | ``` 43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/website/src/components/Alphabetical.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { isContent } from "@lib/braindb.mjs"; 3 | import { getBrainDb } from "starlight-digital-garden"; 4 | import type { Document } from "@braindb/core"; 5 | 6 | const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!); 7 | 8 | const docsByChar = new Map(); 9 | (await getBrainDb().documents()).forEach((doc) => { 10 | if (isContent(doc)) { 11 | const char = firstChar(doc.title()).toUpperCase(); 12 | docsByChar.set(char, docsByChar.get(char) || []); 13 | docsByChar.get(char)?.push(doc); 14 | } 15 | }); 16 | 17 | const comparator = new Intl.Collator("en"); 18 | const charsSorted = [...docsByChar.keys()].sort(comparator.compare); 19 | --- 20 | 21 |
22 | 23 | { 24 | charsSorted.map((char) => ( 25 |

26 | {char} 27 |

    28 | {docsByChar.get(char)?.map((doc) => ( 29 |
  • 30 | {doc.title()} 31 |
  • 32 | ))} 33 |
34 |

35 | )) 36 | } 37 |
38 |
39 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/math-support-in-markdown.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Math support in Markdown 3 | tags: [markdown] 4 | description: Easily use mathematical notation (aka LaTeX) in Markdown without client-side JavaScript 5 | --- 6 | 7 | **aka**: LaTeX, KaTeX, MathJAX 8 | 9 | ## Installation 10 | 11 | import { Steps } from "@astrojs/starlight/components"; 12 | 13 | 14 | 15 | 1. Install dependencies 16 | 17 | ```bash 18 | pnpm add starlight-katex 19 | ``` 20 | 21 | 2. Configure Astro 22 | 23 | ```js {2, 7} 24 | // astro.config.mjs 25 | import { starlightKatex } from "starlight-katex"; 26 | 27 | export default defineConfig({ 28 | integrations: [ 29 | starlight({ 30 | plugins: [starlightKatex()], 31 | }), 32 | ], 33 | }); 34 | ``` 35 | 36 | 37 | 38 | ## Example 39 | 40 | ```md 41 | // example.md 42 | When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are 43 | 44 | $$ 45 | x = {-b \pm \sqrt{b^2-4ac} \over 2a} 46 | $$ 47 | ``` 48 | 49 | When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are 50 | 51 | $$ 52 | x = {-b \pm \sqrt{b^2-4ac} \over 2a} 53 | $$ 54 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/graph-client.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Client-side content graph 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: Relationships between notes visualised as graph, aka network 7 | --- 8 | 9 | :::caution 10 | This page is a work in progress. 11 | ::: 12 | 13 | This is an example of client-side generation of a content graph (in contrast to [[graph|server-side generated graph]]). 14 | 15 | For now, it uses Sigma.js; alternatively, you can use: 16 | 17 | - [Apache ECharts](https://echarts.apache.org/examples/en/index.html#chart-type-graph) 18 | - [Cytoscape](https://js.cytoscape.org/) 19 | - [D3-force](https://d3js.org/d3-force) 20 | - or [others](https://graph.stereobooster.com/notes/Visualisation/) 21 | 22 | **Todo**: 23 | 24 | - [ ] Links for nodes 25 | - [ ] Zoom behavior is a bit annoying; is there a way to customize it? 26 | - [ ] Tags probably should have a constant size or damping factor; otherwise, they will always be the biggest nodes. 27 | - [ ] On node hover, highlight neighbor nodes and connecting edges. 28 | - [ ] If there were metadata for tags or pages, like color or icon, it could be used for graph visualization. 29 | 30 | import GraphClient from "@components/GraphClient.astro"; 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/Backlinks.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getBrainDb } from "@braindb/astro"; 3 | // import type { StarlightDocsEntry } from "@astrojs/starlight/utils/routing"; 4 | interface Props { 5 | entry: any //StarlightDocsEntry; 6 | } 7 | const { entry } = Astro.props; 8 | 9 | const doc = await getBrainDb().findDocument(entry.filePath.replace("src/content/docs", "")); 10 | const links = 11 | doc 12 | ?.documentsFrom() 13 | .filter((doc) => doc.frontmatter().draft !== true || import.meta.env.DEV) || 14 | []; 15 | --- 16 | 17 | { 18 | links.length > 0 && ( 19 | 29 | ) 30 | } 31 | 32 | 53 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/sortable-tables.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sortable tables 3 | tags: [component] 4 | sidebar: 5 | label: Sortable tables 🚷 6 | description: How to add a sortable table to a static website 7 | --- 8 | 9 | ## Ideas 10 | 11 | - [Sticky Header](https://css-tricks.com/position-sticky-and-table-headers/) 12 | 13 | ## JS Libraries 14 | 15 | - [LeeWannacott/table-sort-js](https://github.com/LeeWannacott/table-sort-js) 16 | - [tofsjonas/sortable](https://github.com/tofsjonas/sortable) 17 | - [hubspot/sortable](https://github.hubspot.com/sortable/docs/welcome/) 18 | - [kryogenix/sorttable](https://www.kryogenix.org/code/browser/sorttable/) 19 | - [List.js](https://listjs.com/examples/table/) 20 | 21 | ## Reference Implementations 22 | 23 | - [Tablesorter](https://www.mediawiki.org/wiki/Tablesorter) 24 | - [Help: Sorting](https://meta.wikimedia.org/wiki/Help:Sorting) 25 | - [jQuery Tablesorter 2.0](https://mottie.github.io/tablesorter/docs/#Demo) 26 | 27 | ## Related 28 | 29 | - [Responsive Table - Vertical Scroll and Pinned Top Row](https://github.com/withastro/starlight/discussions/961) 30 | 31 | ## a11y 32 | 33 | - https://www.a11yiseverything.com/articles/sortable-data-tables/ 34 | - https://adrianroselli.com/2021/04/sortable-table-columns.html 35 | - https://carbondesignsystem.com/components/data-table/accessibility/ 36 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/gfm-aside.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: GFM Aside 3 | tags: [markdown, gfm] 4 | description: aka admonitions, callouts 5 | --- 6 | 7 | **aka**: admonitions, callouts 8 | 9 | [GitHub-flavored Markdown supports a special style of blockquotes](https://github.com/orgs/community/discussions/16925): 10 | 11 | ```md 12 | > [!NOTE] 13 | > Highlights information that users should take into account, even when skimming. 14 | ``` 15 | 16 | ## Starlight 17 | 18 | In Starlight, the [syntax is different](https://starlight.astro.build/guides/authoring-content/#asides): 19 | 20 | ```md 21 | :::note 22 | Highlights information that users should take into account, even when skimming. 23 | ::: 24 | ``` 25 | 26 | :::note 27 | Highlights information that users should take into account, even when skimming. 28 | ::: 29 | 30 | ## Related discussions 31 | 32 | - [GitHub Markdown Alerts Support](https://github.com/withastro/starlight/discussions/1884) 33 | - [remark-github-alerts](https://github.com/hyoban/remark-github-alerts/blob/main/src/index.ts) 34 | - [Starlight Asides](https://github.com/withastro/starlight/blob/main/packages/starlight/integrations/asides.ts) 35 | - [Improve the Astro:Content `` Render Props API](https://github.com/withastro/roadmap/discussions/769) 36 | - [How to Customize Markdown with Astro Components?](https://github.com/withastro/roadmap/discussions/423) 37 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/svgpanzoom.css: -------------------------------------------------------------------------------- 1 | .pan-zoom-button { 2 | outline: none; 3 | margin: 0; 4 | width: auto; 5 | overflow: visible; 6 | 7 | background: transparent; 8 | 9 | /* inherit font & color from ancestor */ 10 | font: inherit; 11 | 12 | /* Corrects font smoothing for webkit */ 13 | -webkit-font-smoothing: inherit; 14 | -moz-osx-font-smoothing: inherit; 15 | 16 | /* Corrects inability to style clickable input types in iOS */ 17 | -webkit-appearance: none; 18 | appearance: none; 19 | 20 | color: var(--sl-color-white); 21 | background-color: var(--sl-color-bg); 22 | margin-block: 1rem; 23 | margin-inline-end: 1rem; 24 | align-items: center; 25 | border: 1px solid var(--sl-color-white); 26 | border-radius: 999rem; 27 | display: inline-flex; 28 | font-size: var(--sl-text-sm); 29 | gap: 0.5em; 30 | line-height: 1.1875; 31 | outline-offset: 0.25rem; 32 | padding: 0.4375rem 1.125rem; 33 | cursor: pointer; 34 | } 35 | .pan-zoom-button:active { 36 | background-color: var(--sl-color-accent); 37 | color: var(--sl-color-black); 38 | border-color: transparent; 39 | } 40 | .pan-zoom-button::-moz-focus-inner { 41 | border: 0; 42 | padding: 0; 43 | } 44 | .pan-zoom-button:focus { 45 | outline: none; 46 | } 47 | .pan-zoom-button:focus:not(:focus-visible) { 48 | outline: none; 49 | } 50 | .pan-zoom-button:focus:not(:-moz-focusring) { 51 | outline: none; 52 | } 53 | -------------------------------------------------------------------------------- /packages/website/src/components/Graph.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { renderer, DEFAULTS } from "./graphRenderer"; 3 | 4 | export interface Props { 5 | graph: any; 6 | } 7 | 8 | const { graph } = Astro.props; 9 | 10 | // probably need to use cache here 11 | const result = renderer(graph, { 12 | ...DEFAULTS, 13 | width: 800, 14 | height: 800, 15 | }); 16 | --- 17 | 18 |
19 | 20 |
21 | 22 | 55 | -------------------------------------------------------------------------------- /packages/website/src/pages/og/[...slug].ts: -------------------------------------------------------------------------------- 1 | import { getCollection } from "astro:content"; 2 | import { OGImageRoute } from "astro-og-canvas"; 3 | 4 | // Get all entries from the `docs` content collection. 5 | const entries = await getCollection("docs"); 6 | 7 | // Map the entry array to an object with the page ID as key and the 8 | // frontmatter data as value. 9 | const pages = Object.fromEntries(entries.map(({ data, id }) => [id, { data }])); 10 | 11 | export const { getStaticPaths, GET } = OGImageRoute({ 12 | // Pass down the documentation pages. 13 | pages, 14 | // Define the name of the parameter used in the endpoint path, here `slug` 15 | // as the file is named `[...slug].ts`. 16 | param: "slug", 17 | // Define a function called for each page to customize the generated image. 18 | getImageOptions: (_path, page: (typeof pages)[number]) => { 19 | return { 20 | // Use the page title and description as the image title and description. 21 | title: page.data.title, 22 | description: [ 23 | page.data.description, 24 | page.data.tags 25 | ?.sort() 26 | .map((x) => `#${x}`) 27 | .join(" · "), 28 | ] 29 | .filter(Boolean) 30 | .join("\n\n"), 31 | // Customize various colors and add a border. 32 | bgGradient: [[35, 38, 47]], 33 | border: { color: [179, 199, 255], width: 20 }, 34 | padding: 120, 35 | }; 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /packages/website/src/styles/custom.css: -------------------------------------------------------------------------------- 1 | /* needed if you use strategy: "img-class-dark-mode" */ 2 | html[data-theme="light"] .beoe-dark { 3 | display: none; 4 | } 5 | 6 | html[data-theme="dark"] .beoe-light { 7 | display: none; 8 | } 9 | 10 | /* I had to put this style in global scope, because otherwise it doesn't work in link previews */ 11 | .graphology { 12 | text { 13 | fill: var(--sl-color-white); 14 | } 15 | line { 16 | stroke-width: 2px; 17 | } 18 | } 19 | 20 | /* I had to put this style in global scope, because otherwise it doesn't work in link previews */ 21 | .graphviz { 22 | text { 23 | fill: var(--sl-color-white); 24 | } 25 | [fill="black"], 26 | [fill="#000"] { 27 | fill: var(--sl-color-white); 28 | } 29 | [stroke="black"], 30 | [stroke="#000"] { 31 | stroke: var(--sl-color-white); 32 | } 33 | } 34 | 35 | .gnuplot { 36 | background: #fff; 37 | } 38 | 39 | .euler { 40 | background: #fff; 41 | border-radius: 0.5em; 42 | } 43 | 44 | .column-list { 45 | column-width: calc(var(--sl-content-width) / 2 - 1.5rem); 46 | 47 | ul { 48 | padding: 0; 49 | list-style: none; 50 | margin-top: 0 !important; 51 | } 52 | } 53 | 54 | @media (min-width: 72rem) { 55 | .sl-container { 56 | margin-inline: var(--sl-content-margin-inline, 0) !important; 57 | } 58 | } 59 | 60 | svg.icon.text { 61 | display: inline-block !important; 62 | margin: 0 0 -0.25rem 0 !important; 63 | } 64 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/svgpanzoom.ts: -------------------------------------------------------------------------------- 1 | import "@beoe/pan-zoom/css/PanZoomUi.css"; 2 | import "./svgpanzoom.css"; 3 | import { PanZoomUi } from "@beoe/pan-zoom"; 4 | 5 | // TODO: astro:page-load 6 | document.querySelectorAll(".beoe").forEach((container) => { 7 | const element = container.firstElementChild; 8 | if (!element) return; 9 | new PanZoomUi({ 10 | // @ts-expect-error 11 | element, 12 | // @ts-expect-error 13 | container, 14 | classes: { 15 | zoomIn: "pan-zoom-button", 16 | reset: "pan-zoom-button", 17 | zoomOut: "pan-zoom-button", 18 | buttons: "buttons", 19 | tsWarning: "touchscreen-warning", 20 | tsWarningActive: "active", 21 | }, 22 | }).on(); 23 | }); 24 | 25 | // document 26 | // .querySelectorAll( 27 | // ".sl-markdown-content img[src$='.svg' i]," + 28 | // ".sl-markdown-content img[src$='f=svg' i]" // for development environment 29 | // // ".sl-markdown-content img[src^='data:image/svg+xml']" 30 | // ) 31 | // .forEach((element) => { 32 | // if (element.parentElement?.tagName === "PICTURE") { 33 | // element = element.parentElement; 34 | // } 35 | // const container = document.createElement("figure"); 36 | // container.classList.add("beoe", "not-content"); 37 | // element.replaceWith(container); 38 | // container.append(element); 39 | // // @ts-expect-error 40 | // new PanZoomUi({ element, container }).on(); 41 | // }); 42 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/rehype-plugins-for-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Rehype Plugins for Code 3 | tags: [markdown] 4 | description: How to use Rehype code plugins with Astro or Starlight 5 | --- 6 | 7 | If you want to use a Rehype plugin with Astro to process code blocks (**aka** code fences), like those in Markdown: 8 | 9 | ````md 10 | ``` 11 | something 12 | ``` 13 | ```` 14 | 15 | Or like this in HTML: 16 | 17 | ```html 18 |
19 |     something
20 | 
21 | ``` 22 | 23 | You may need to use special configuration because Astro (and Starlight) comes with a built-in [[syntax-highlighting|code highlighter]]. 24 | 25 | ## Astro 26 | 27 | To use it with Astro, you need to disable the built-in syntax highlighting and apply your plugin afterward: 28 | 29 | ```js {2, 6, 8-9} 30 | // astro.config.mjs 31 | import { rehypeShiki, markdownConfigDefaults } from "@astrojs/markdown-remark"; 32 | 33 | export default defineConfig({ 34 | markdown: { 35 | syntaxHighlight: false, 36 | rehypePlugins: [ 37 | yourPlugin, 38 | [rehypeShiki, markdownConfigDefaults.shikiConfig], 39 | ], 40 | }, 41 | }); 42 | ``` 43 | 44 | ## Starlight 45 | 46 | **Important** use Starlight [v0.22+](https://github.com/withastro/starlight/discussions/1259#discussioncomment-9300105) 47 | 48 | ```js {5} 49 | // astro.config.mjs 50 | export default defineConfig({ 51 | integrations: [starlight({})], 52 | markdown: { 53 | rehypePlugins: [yourPlugin], 54 | }, 55 | }); 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro-digital-garden-website", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro", 11 | "postinstall": "playwright install chromium" 12 | }, 13 | "dependencies": { 14 | "@astrojs/check": "^0.9.4", 15 | "@astrojs/markdown-remark": "^6.3.2", 16 | "@astrojs/starlight": "^0.34.3", 17 | "@beoe/cache": "^0.1.1", 18 | "@beoe/rehype-gnuplot": "^0.5.0", 19 | "@beoe/rehype-graphviz": "^0.4.2", 20 | "@beoe/rehype-mermaid": "^0.4.2", 21 | "@braindb/core": "0.0.17", 22 | "@iconify/json": "^2.2.341", 23 | "@pagefind/default-ui": "^1.3.0", 24 | "astro": "^5.8.0", 25 | "astro-og-canvas": "^0.7.0", 26 | "astro-seo-schema": "^5.0.0", 27 | "canvaskit-wasm": "^0.40.0", 28 | "edeap": "0.0.3", 29 | "graphology": "^0.26.0", 30 | "graphology-layout": "^0.6.1", 31 | "graphology-layout-forceatlas2": "^0.10.1", 32 | "graphology-svg": "^0.1.3", 33 | "graphology-utils": "^2.5.2", 34 | "playwright": "^1.52.0", 35 | "rehype-autolink-headings": "^7.1.0", 36 | "schema-dts": "^1.1.5", 37 | "sharp": "^0.34.2", 38 | "sigma": "3.0.1", 39 | "starlight-digital-garden": "workspace:*", 40 | "starlight-katex": "workspace:*", 41 | "typescript": "^5.8.3", 42 | "unplugin-icons": "^22.1.0", 43 | "venn-isomorphic": "^0.0.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/README.md: -------------------------------------------------------------------------------- 1 | # `starlight-digital-garden` 2 | 3 | Astro digital garden - set of plugins (Astro, Remark, Rehype) and Astro components that I use often: 4 | 5 | - [BrainDB](https://astro-digital-garden.stereobooster.com/recipes/braindb/) 6 | - [Remark plugin to resolve wiki links](https://astro-digital-garden.stereobooster.com/recipes/wikilinks/) 7 | - [Backlinks (in the left sidebar)](https://astro-digital-garden.stereobooster.com/recipes/backlinks/) 8 | - [Link previews](https://astro-digital-garden.stereobooster.com/recipes/link-previews/) 9 | - [Pan and zoom for diagrams](https://astro-digital-garden.stereobooster.com/recipes/svg-pan-zoom/) 10 | - [Rehype plugin to show icon for external link](https://astro-digital-garden.stereobooster.com/recipes/icons-to-external-links/) 11 | - `astro-robots-txt` 12 | - [remark-dataview](https://astro-digital-garden.stereobooster.com/recipes/obsidian-dataview/) (disabled by default) 13 | 14 | ## Installation 15 | 16 | ```sh 17 | pnpm add starlight-digital-garden @braindb/core 18 | ``` 19 | 20 | Change configuration in `astro.config.mjs`: 21 | 22 | ```js 23 | import { defineConfig } from "astro/config"; 24 | import starlight from "@astrojs/starlight"; 25 | import starlightDigitalGarden from "starlight-digital-garden"; 26 | 27 | export default defineConfig({ 28 | integrations: [ 29 | starlight({ 30 | plugins: [starlightDigitalGarden()], 31 | }), 32 | ], 33 | }); 34 | ``` 35 | 36 | ## License 37 | 38 | Licensed under the MIT License, Copyright © stereobooster. 39 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starlight-digital-garden", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "description": "Digital garden for starlight", 6 | "author": "stereobooster", 7 | "type": "module", 8 | "keywords": [ 9 | "starlight", 10 | "plugin", 11 | "astro", 12 | "obsidian", 13 | "zettelkasten", 14 | "digital-garden", 15 | "second-brain", 16 | "zettelkasten-methodology", 17 | "digital-gardening" 18 | ], 19 | "exports": { 20 | ".": "./index.ts", 21 | "./components/Backlinks.astro": "./components/Backlinks.astro", 22 | "./components/LinkPreview.astro": "./components/LinkPreview.astro", 23 | "./overrides/PageFrame.astro": "./overrides/PageFrame.astro", 24 | "./overrides/TableOfContents.astro": "./overrides/TableOfContents.astro" 25 | }, 26 | "devDependencies": { 27 | "@astrojs/starlight": "^0.32.5", 28 | "astro": "^5.5.5" 29 | }, 30 | "peerDependencies": { 31 | "@astrojs/starlight": ">=0.32.5" 32 | }, 33 | "engines": { 34 | "node": "^18.17.1 || ^20.3.0 || >=21.0.0" 35 | }, 36 | "publishConfig": { 37 | "access": "public" 38 | }, 39 | "repository": { 40 | "type": "git", 41 | "url": "https://github.com/stereobooster/astro-digital-garden.git", 42 | "directory": "packages/starlight-digital-garden" 43 | }, 44 | "dependencies": { 45 | "@beoe/pan-zoom": "^0.0.4", 46 | "@braindb/astro": "^0.1.1", 47 | "@braindb/remark-dataview": "^0.0.2", 48 | "@floating-ui/dom": "^1.6.13", 49 | "astro-integration-kit": "^0.18.0", 50 | "astro-robots-txt": "^1.0.0", 51 | "rehype-external-links": "^3.0.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/social-images-autogenration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Social images autogeneration 3 | tags: [component] 4 | description: Automatically generate social images, aka Open Graph images and X/Twitter cards 5 | --- 6 | 7 | **Aka** [Open Graph](https://ogp.me/) images and [X/Twitter Card](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) images. 8 | 9 | ## Options 10 | 11 | The basic idea is to create an HTML or SVG "image" for a page based on metadata, such as `title`, `description`, `tags`, etc. Then, convert it to a raster image. There are several options: 12 | 13 | - [Satori](https://github.com/vercel/satori) 14 | - [CanvasKit - Skia](https://skia.org/docs/user/modules/canvaskit/) 15 | - [resvg-js](https://github.com/yisibl/resvg-js) 16 | - Headless browsers 17 | 18 | ### Satori 19 | 20 | - Example in [astro-theme-cactus](https://github.com/chrismwilliams/astro-theme-cactus/blob/main/src/pages/og-image/%5Bslug%5D.png.ts) 21 | - Example in [astro-paper](https://github.com/satnaing/astro-paper/pull/15/files) 22 | 23 | ### CanvasKit 24 | 25 | - [Astro OG Canvas](https://github.com/delucis/astro-og-canvas) 26 | 27 | ## Installation 28 | 29 | [Full instructions for Astro OG Canvas](https://hideoo.dev/notes/starlight-og-images) 30 | 31 | ## Related 32 | 33 | - [[seo-and-smo-meta-tags]] 34 | - [MetaTags.io](https://metatags.io/) 35 | 36 | ## Further Improvements 37 | 38 | The current implementation looks boring. Maybe I can: 39 | 40 | - [ ] Generate a gradient based on tag colors 41 | - Use "cover" images from some pages in social images, although all of them are SVGs 42 | - It seems that `Astro OG Canvas` doesn't support emojis 43 | - Add a logo 44 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/search-for-static-website.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Search for static website 3 | tags: [component] 4 | description: Different options for full-text search for static websites without backend or third-party services 5 | --- 6 | 7 | ## Starlight 8 | 9 | > By default, Starlight sites include full-text search powered by Pagefind, which is a fast and low-bandwidth search tool for static sites. 10 | > 11 | > -- [Starlight Site Search](https://starlight.astro.build/guides/site-search/) 12 | 13 | ## Astro 14 | 15 | - For [Pagefind](https://pagefind.app/), see [Starlight](https://github.com/withastro/starlight/) 16 | - [Uses generated HTML](https://github.com/withastro/starlight/blob/d2822a1127c622e086ad8877a07adad70d8c3aab/packages/starlight/index.ts#L61-L72) 17 | - [Minisearch](https://github.com/Barnabas/astro-minisearch/) 18 | - [Uses source files](https://github.com/Barnabas/astro-minisearch/blob/main/demo/src/pages/search.json.js#L11-L17) 19 | - [Fuse](https://github.com/johnny-mh/blog2/tree/main/packages/astro-fuse) 20 | - Can use [source files](https://github.com/johnny-mh/blog2/blob/main/packages/astro-fuse/src/basedOnSource.ts) 21 | - And [generated HTML](https://github.com/johnny-mh/blog2/blob/main/packages/astro-fuse/src/basedOnOutput.ts) 22 | - [Lunr](https://github.com/jackcarey/astro-lunr) 23 | - [Uses generated HTML](https://github.com/jackcarey/astro-lunr/blob/master/src/index.ts) 24 | - [Flexsearch](https://github.com/nextapps-de/flexsearch) 25 | - **TODO** Need to find an example of integration with Astro. 26 | - [Orama](https://docs.oramasearch.com/open-source/plugins/plugin-astro) 27 | - [Uses generated HTML](https://github.com/oramasearch/orama/blob/main/packages/plugin-astro/src/index.ts) 28 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/recently-changed-pages.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Recently changed pages 3 | tags: [page, component] 4 | description: Astro component to display recently changed pages 5 | --- 6 | 7 | ## Installation 8 | 9 | import { Steps } from "@astrojs/starlight/components"; 10 | 11 | 12 | 13 | 1. Install [[braindb#installation]] 14 | 2. Create `RecentChanges` component 15 | 16 | ```astro 17 | //src/components/RecentChanges.astro 18 | --- 19 | import { getBrainDb } from "@braindb/astro"; 20 | import type { Document } from "@braindb/core"; 21 | 22 | const docs = await getBrainDb().documents({ sort: ["updated_at", "desc"] }); 23 | 24 | const docsByDate = new Map(); 25 | docs.filter((doc) => doc.frontmatter().draft !== true || import.meta.env.DEV).forEach((doc) => { 26 | const date = doc.updatedAt().toISOString().split("T")[0]; 27 | docsByDate.set(date, docsByDate.get(date) || []); 28 | docsByDate.get(date)?.push(doc); 29 | }); 30 | const dates = Array.from(docsByDate.keys()).map((d) => [ 31 | d, 32 | d.split("-").reverse().join("."), 33 | ]); 34 | --- 35 | 36 |
37 | 38 | { 39 | dates.map(([key, title]) => ( 40 |

41 | {title} 42 |

    43 | {docsByDate.get(key)?.map((doc) => ( 44 |
  • 45 | {doc.title()} 46 |
  • 47 | ))} 48 |
49 |

50 | )) 51 | } 52 |
53 |
54 | ``` 55 | 56 | 3. **Use component**, wherever you like 57 | 58 |
59 | 60 | ## Example 61 | 62 | See [[recent]]. 63 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/alphabetical-index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alphabetical index 3 | tags: [page] 4 | description: Alphabetical list of all notes on the website, similar to a glossary in a book 5 | --- 6 | 7 | **aka** glossary 8 | 9 | ## Implementation 10 | 11 | import { Steps } from "@astrojs/starlight/components"; 12 | 13 | 14 | 15 | 1. Create `Alphabetical` component 16 | 17 | ```astro 18 | // src/components/Alphabetical.astro 19 | --- 20 | import { getCollection } from "astro:content"; 21 | 22 | const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!); 23 | 24 | const docs = await getCollection("docs"); 25 | type Docs = typeof docs; 26 | const docsByChar = new Map(); 27 | docs.forEach((doc) => { 28 | const char = firstChar(doc.data.title).toUpperCase(); 29 | docsByChar.set(char, docsByChar.get(char) || []); 30 | docsByChar.get(char)?.push(doc); 31 | }); 32 | const comparator = new Intl.Collator("en"); 33 | const charsSorted = [...docsByChar.keys()].sort(comparator.compare); 34 | --- 35 | 36 | { 37 | charsSorted.map((char) => ( 38 | <> 39 |

{char}

40 |
    41 | {docsByChar.get(char)?.map((doc) => ( 42 |
  • 43 | {doc.data.title} 44 |
  • 45 | ))} 46 |
47 | 48 | )) 49 | } 50 | ``` 51 | 52 | 2. **Use component**, wherever you like 53 | 54 | ```mdx 55 | --- 56 | title: Alphabetical index 57 | tableOfContents: false 58 | prev: false 59 | next: false 60 | --- 61 | 62 | import Alphabetical from "@components/Alphabetical.astro"; 63 | 64 | 65 | ``` 66 | 67 |
68 | 69 | ## Example 70 | 71 | See [[alphabetical]]. 72 | -------------------------------------------------------------------------------- /packages/website/src/components/GraphClient.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 |
6 | 7 | 13 | 14 | 64 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/table-of-contents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Table of Contents 3 | tags: [component, markdown] 4 | description: How to create a table of contents based on headings from a Markdown file. 5 | --- 6 | 7 | ## Starlight 8 | 9 | Starlight already has this feature, so no additional work is required. However, you can [override the default one](https://starlight.astro.build/reference/overrides/#tableofcontents). 10 | 11 | ## "Snake" Table of Contents 12 | 13 | See: 14 | 15 | - https://kld.dev/building-table-of-contents/ 16 | - https://kld.dev/toc-animation/ 17 | - [fumadocs toc-clerk.tsx](https://github.com/fuma-nama/fumadocs/blob/dev/packages/ui/src/components/layout/toc-clerk.tsx) 18 | 19 | ### Implementation 20 | 21 | Check out the source code to see how to implement it: 22 | 23 | - [src/components/TOC.astro](https://github.com/stereobooster/astro-digital-garden/tree/main/packages/website/src/components/TOC.astro) 24 | - [src/components/TOCHeading.astro](https://github.com/stereobooster/astro-digital-garden/tree/main/packages/website/src/components/TOCHeading.astro) 25 | - [src/toc.js](https://github.com/stereobooster/astro-digital-garden/tree/main/packages/website/src/components/toc.js) 26 | 27 | ### TODO: 28 | 29 | - [ ] Fix "snake" styles. 30 | 31 | ### Starlight-Specific Config 32 | 33 | import { Steps } from "@astrojs/starlight/components"; 34 | 35 | 36 | 1. Create `TableOfContents` component 37 | 38 | ```astro 39 | // src/components/TableOfContents.astro 40 | --- 41 | import TOC from "./TOC.astro"; 42 | --- 43 | 44 | 45 | ``` 46 | 47 | 2. Configure Astro 48 | 49 | ```js {6} 50 | // astro.config.mjs 51 | export default defineConfig({ 52 | integrations: [ 53 | starlight({ 54 | components: { 55 | TableOfContents: "./src/components/TableOfContents.astro", 56 | }, 57 | }), 58 | ], 59 | }); 60 | ``` 61 | 62 | 63 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/icons.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Icons 3 | tags: [component] 4 | description: Access thousands of icons as components on demand 5 | --- 6 | 7 | ## Installation 8 | 9 | import { Steps } from "@astrojs/starlight/components"; 10 | 11 | 12 | 13 | 1. Install dependencies 14 | 15 | ```bash 16 | pnpm add unplugin-icons @iconify/json 17 | ``` 18 | 19 | 2. Configure Astro 20 | 21 | ```js {2, 7-9} 22 | // astro.config.mjs 23 | import Icons from "unplugin-icons/vite"; 24 | 25 | export default defineConfig({ 26 | vite: { 27 | plugins: [ 28 | Icons({ 29 | compiler: "astro", 30 | }), 31 | ], 32 | }, 33 | }); 34 | ``` 35 | 36 | 3. Configure TypeScript (if you use TypeScript) 37 | 38 | ```json {5} 39 | // tsconfig.json 40 | { 41 | "extends": "astro/tsconfigs/strict", 42 | "compilerOptions": { 43 | "types": ["unplugin-icons/types/astro"] 44 | } 45 | } 46 | ``` 47 | 48 | 49 | 50 | ## Example 51 | 52 | ```mdx 53 | import AstroLogo from "~icons/logos/astro-icon"; 54 | 55 | 56 | ``` 57 | 58 | import AstroLogo from "~icons/logos/astro-icon"; 59 | 60 | 61 | 62 | ## Related 63 | 64 | - [VSCode extension](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) ([code](https://github.com/antfu/vscode-iconify)) 65 | - [Icon Explorer](https://icones.js.org/) ([code](https://github.com/antfu/icones)) 66 | - [Astro Icon](https://www.astroicon.dev/) 67 | 68 | ## Further improvements 69 | 70 | - [unplugin-svg-sprite](https://github.com/yunsii/unplugin-svg-sprite) 71 | - [feat: generate svg sprite from used icons](https://github.com/unplugin/unplugin-icons/issues/55) 72 | - [Iconify for Tailwind CSS](https://iconify.design/docs/usage/css/tailwind/) 73 | - [tailwindcss-plugin-iconify](https://github.com/yunsii/tailwindcss-plugin-iconify) 74 | -------------------------------------------------------------------------------- /packages/starlight-katex/README.md: -------------------------------------------------------------------------------- 1 | # Starlight plugin for Katex 2 | 3 | ## Starlight 4 | 5 | ### Installation 6 | 7 | ```sh 8 | pnpm add starlight-katex 9 | ``` 10 | 11 | Change configuration in `astro.config.mjs`: 12 | 13 | ```js 14 | import { defineConfig } from "astro/config"; 15 | import starlight from "@astrojs/starlight"; 16 | import { starlightKatex } from "starlight-katex"; 17 | 18 | export default defineConfig({ 19 | integrations: [ 20 | starlight({ 21 | plugins: [starlightKatex()], 22 | }), 23 | ], 24 | }); 25 | ``` 26 | 27 | ## Astro 28 | 29 | There is also plugin, for Asto. If you use Starlight plugin you don't need it! 30 | 31 | ### Installation 32 | 33 | ```sh 34 | pnpm add katex starlight-katex 35 | ``` 36 | 37 | Change configuration in `astro.config.mjs`: 38 | 39 | ```js 40 | import { defineConfig } from "astro/config"; 41 | import { astroKatex } from "starlight-katex"; 42 | 43 | export default defineConfig({ 44 | integrations: [astroKatex()], 45 | }); 46 | ``` 47 | 48 | But you also need to load CSS `katex/dist/katex.min.css`. 49 | 50 | ## Usage 51 | 52 | Use `$$` or `$$$$`. For example, 53 | 54 | ```md 55 | When $a \ne 0$, there are two solutions to $(ax^2 + bx + c = 0)$ and they are 56 | 57 | $$ 58 | x = {-b \pm \sqrt{b^2-4ac} \over 2a} 59 | $$ 60 | ``` 61 | 62 | ## PS 63 | 64 | This plugin is just shortcut for following configuration: 65 | 66 | ```sh 67 | pnpm add katex rehype-katex remark-math 68 | ``` 69 | 70 | ```js 71 | // astro.config.mjs 72 | import remarkMath from "remark-math"; 73 | import rehypeKatex from "rehype-katex"; 74 | import addClasses from "rehype-class-names"; 75 | 76 | export default defineConfig({ 77 | integrations: [ 78 | starlight({ 79 | customCss: ["katex/dist/katex.min.css"], 80 | }), 81 | ], 82 | markdown: { 83 | remarkPlugins: [remarkMath], 84 | rehypePlugins: [rehypeKatex, [addClasses, { ".katex": "not-content" }]], 85 | }, 86 | vite: { 87 | ssr: { 88 | noExternal: ["katex"], 89 | }, 90 | }, 91 | }); 92 | ``` 93 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/tasks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tasks 3 | tableOfContents: false 4 | prev: false 5 | next: false 6 | description: My todo list 7 | --- 8 | 9 | - [ ] Write about `@beoe/rehype-vizdom` 10 | - Write about interactivity 11 | - [ ] Write about `@beoe/rehype-d2` 12 | - [ ] Add more options for `starlight-digital-garden` 13 | - option to check for broken links? 14 | - [ ] Write about `starlight-digital-garden` 15 | - [ ] Starlight SEO/SMO plugin 16 | - `astro-robots-txt` 17 | - microdata 18 | - social image 19 | - date in sitemap 20 | - [ ] Add a grammar checker 21 | - [ ] Implement [[faceted-search]] 22 | - [ ] Add [View Transitions](https://docs.astro.build/en/guides/view-transitions/) 23 | - See [starlight#694](https://github.com/withastro/starlight/pull/694#issuecomment-2021611520) 24 | - [ ] Implement better user action tracking (beyond page navigations) 25 | - If search was used 26 | - If external links were clicked 27 | - If page preview was shown 28 | - [ ] Remove H1 on the tags page 29 | 30 | ## Ideas 31 | 32 | - Footnotes: 33 | - Mention how footnotes can be shown on the side, like in [Tufte design](https://edwardtufte.github.io/tufte-css/) 34 | - See https://gwern.net/sidenote 35 | 36 | ## Color for Tag 37 | 38 | I can easily generate a color from the tag: 39 | 40 | ```js 41 | import ch from "color-hash"; 42 | const colorHash = new ch.default(); 43 | colorHash.hex(tag); 44 | ``` 45 | 46 | I can use it in: 47 | 48 | - [x] Color chip near the tag or maybe a color underline? 49 | - [x] Euler diagram 50 | - [x] Content graph 51 | - [ ] Social images - to generate a colorful "border" 52 | 53 | I implemented a prototype in the branch [tag-color](https://github.com/stereobooster/astro-digital-garden/tree/tag-color). 54 | 55 | ## From Other Articles 56 | 57 | ```dataview list 58 | SELECT dv_link(), dv_task() 59 | FROM tasks JOIN documents ON documents.path = tasks.from 60 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 61 | AND url != '/tasks/' 62 | AND checked = false 63 | ORDER BY updated_at DESC, path, tasks.start; 64 | ``` 65 | -------------------------------------------------------------------------------- /packages/starlight-katex/src/index.ts: -------------------------------------------------------------------------------- 1 | import remarkMath from "remark-math"; 2 | import rehypeKatex from "rehype-katex"; 3 | import addClasses from "rehype-class-names"; 4 | import { defineIntegration } from "astro-integration-kit"; 5 | import { z } from "astro/zod"; 6 | import type { StarlightPlugin } from "@astrojs/starlight/types"; 7 | import * as importMetaResolve from "import-meta-resolve"; 8 | 9 | const katexCss = importMetaResolve 10 | .resolve("katex/dist/katex.min.css", import.meta.url) 11 | .replace("file://", ""); 12 | 13 | // I can inject CSS (katex/dist/katex.min.css) only with Starlight 14 | export const astroKatex = defineIntegration({ 15 | name: "astro-katex", 16 | optionsSchema: z.object({}).optional(), 17 | setup() { 18 | return { 19 | hooks: { 20 | "astro:config:setup": ({ config, updateConfig }) => { 21 | const noExternal = config.vite.ssr?.noExternal; 22 | 23 | const newConfig = { 24 | markdown: { 25 | remarkPlugins: [...config.markdown.remarkPlugins, remarkMath], 26 | rehypePlugins: [ 27 | ...config.markdown.rehypePlugins, 28 | rehypeKatex, 29 | [addClasses, { ".katex": "not-content" }], 30 | ], 31 | }, 32 | vite: { 33 | ssr: { 34 | noExternal: [ 35 | ...(Array.isArray(noExternal) 36 | ? noExternal 37 | : noExternal && noExternal !== true 38 | ? [noExternal] 39 | : []), 40 | "katex", 41 | ], 42 | }, 43 | }, 44 | }; 45 | updateConfig(newConfig); 46 | }, 47 | }, 48 | }; 49 | }, 50 | }); 51 | 52 | export function starlightKatex(): StarlightPlugin { 53 | return { 54 | name: "starlight-digital-garden", 55 | hooks: { 56 | setup({ config, updateConfig, addIntegration }) { 57 | updateConfig({ 58 | customCss: [...(Array.isArray(config.customCss) ? config.customCss : []), katexCss], 59 | }); 60 | addIntegration(astroKatex()); 61 | }, 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/website/src/lib/graph.ts: -------------------------------------------------------------------------------- 1 | import { BrainDB } from "@braindb/core"; 2 | import { isContent } from "./braindb.mjs"; 3 | import { getBrainDb } from "starlight-digital-garden"; 4 | import graphology from "graphology"; 5 | import circular from "graphology-layout/circular"; 6 | import forceAtlas2 from "graphology-layout-forceatlas2"; 7 | // @ts-ignore 8 | const { MultiGraph } = graphology; 9 | 10 | export async function toGraphologyJson(db: BrainDB) { 11 | const nodes = (await db.documents()).filter(isContent).map((document) => ({ 12 | key: document.id(), 13 | attributes: { 14 | label: document.frontmatter().title as string, 15 | url: document.url(), 16 | }, 17 | })); 18 | 19 | const edges = (await db.links()) 20 | .filter( 21 | (link) => 22 | link.to() !== null && isContent(link.to()!) && isContent(link.from()) 23 | ) 24 | .map((link) => ({ 25 | source: link.from().id(), 26 | target: link.to()?.id(), 27 | })); 28 | 29 | const tagsAll = (await db.documents()) 30 | .map((document) => { 31 | const tags = document.frontmatter().tags; 32 | return Array.isArray(tags) ? tags : []; 33 | }) 34 | .flat(); 35 | 36 | const tagNodes = [...new Set(tagsAll)].map((tag) => ({ 37 | key: tag, 38 | attributes: { 39 | label: `#${tag}`, 40 | url: "", 41 | size: 0.4, 42 | }, 43 | })); 44 | 45 | const tagEdges = (await db.documents()) 46 | .map((document) => { 47 | const tags = document.frontmatter().tags; 48 | if (!Array.isArray(tags)) return []; 49 | return tags.map((tag) => ({ 50 | source: tag, 51 | target: document.id(), 52 | })); 53 | }) 54 | .flat(); 55 | 56 | return { 57 | attributes: { name: "g" }, 58 | options: { 59 | allowSelfLoops: true, 60 | multi: true, 61 | type: "directed", 62 | }, 63 | nodes: [...nodes, ...tagNodes], 64 | edges: [...edges, ...tagEdges], 65 | }; 66 | } 67 | 68 | export async function getGraph() { 69 | const graph = new MultiGraph(); 70 | const data = await toGraphologyJson(getBrainDb()); 71 | graph.import(data as any); 72 | circular.assign(graph); 73 | forceAtlas2.assign(graph, 2000); 74 | return graph; 75 | } 76 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Astro Digital Garden 3 | sidebar: 4 | label: Introduction 5 | prev: false 6 | next: false 7 | description: How to publish a digital garden (aka Second brain, Zettelkasten) with Astro 8 | --- 9 | 10 | import { LinkCard, CardGrid } from "@astrojs/starlight/components"; 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ## About Digital Gardens 20 | 21 | [Digital garden](https://github.com/MaggieAppleton/digital-gardeners), also known as a [Second brain](https://www.ssp.sh/brain/), [Zettelkasten](https://en.wikipedia.org/wiki/Zettelkasten), personal wiki, or personal knowledge management. 22 | 23 | There are different aspects of a digital garden. It can be seen as a way to take notes just for yourself, but for me, it's more interesting as a way to organize knowledge and publish it as a (static) website. 24 | 25 | There are various editors (note-taking applications) such as [Obsidian](https://obsidian.md/), [Foam](https://foambubble.github.io/foam/), and [Roam Research](https://roamresearch.com/) to name a few. But again, I am more interested in the publishing aspect—assuming you have already created content with an editor of your choice. 26 | 27 | My previous articles on the subject: 28 | 29 | - [Digital Garden as Static Website](https://stereobooster.com/posts/digital-garden-as-static-website/) 30 | - [Useful Modern Tools for Static Websites](https://stereobooster.com/posts/useful-modern-tools-for-static-websites/) 31 | - [What I Miss in Markdown (and Hugo)](https://stereobooster.com/posts/what-i-miss-in-markdown/) 32 | 33 | ## Goal 34 | 35 | The **ultimate goal** is to create a theme and set of tools to publish a digital garden with the help of Astro. For now, I am collecting relevant code snippets and links. 36 | 37 | ## Notation 38 | 39 | Symbols used in the sidebar: 40 | 41 | - 🧠 - Just an idea. There is no solution or specific action. 42 | - 🚷 - I know what I want to do, but I don't know how exactly. 43 | - 🚧 - I know what I want to do, and I know how to do it. What is left is to actually implement it. 44 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/permalinks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Permanent links 3 | tags: [page] 4 | sidebar: 5 | label: Permanent links 🧠 6 | description: My thoughts and ideas about permanent links 7 | --- 8 | 9 | **Problem**: Links in Starlight (and [[sidebar|autogenerated sidebar]]) are connected to the file path. However, in digital gardens, content often gets moved around, resulting in broken links. 10 | 11 | ## Brainstorming 12 | 13 | ### Redirects 14 | 15 | Every time a page is moved, add the previous version to the [redirects](https://docs.astro.build/en/guides/routing/#redirects) section. 16 | 17 | It would be even better if these [redirects could be specified in the frontmatter](https://github.com/withastro/starlight/discussions/1847), similar to how it’s done in [Hugo](https://gohugo.io/content-management/urls/#aliases). 18 | 19 | ### Bare slugs 20 | 21 | For example, if the path is `some/thing/a.md`, generate the URL as `/a` instead of `some/thing/a`. This allows you to move the file to `other/a.md` without changing the URL. 22 | 23 | Paths can be used for: 24 | 25 | - [Breadcrumbs](https://quartz.jzhao.xyz/features/breadcrumbs) 26 | - [Sidebar](https://starlight.astro.build/guides/sidebar/) 27 | 28 | #### Hack for Starlight 29 | 30 | This behavior can be achieved in Starlight by putting `slug` in the frontmatter: 31 | 32 | ```md 33 | // src/content/docs/recipes/alphabetical-index.mdx 34 | --- 35 | slug: alphabetical-index 36 | --- 37 | ``` 38 | 39 | Related: [starlight#2052](https://github.com/withastro/starlight/discussions/2052) 40 | 41 | ### Permanent Anchors 42 | 43 | Anchors can also break, though this issue is likely less significant compared to broken links. 44 | 45 | One approach is to [specify an ID instead of using a slug](https://www.markdownguide.org/extended-syntax/#heading-ids). This allows you to change the heading text without altering the ID: 46 | 47 | ```md 48 | ### My Great Heading {#custom-id} 49 | ``` 50 | ### Conflicting Pages 51 | 52 | In addition to content pages, there will be some special pages, such as: 53 | 54 | - `/recent` 55 | - `/recent/[page]` 56 | - `/graph` 57 | - `/alphabetical` 58 | - `/tags` 59 | - `/tags/[name]` 60 | - `/search` 61 | - etc. 62 | 63 | In these cases, built-in pages should take priority over content pages, which may require renaming some content pages. However, for `/tags/[name]`, both content and built-in pages can be displayed, similar to how it's handled in Hugo. 64 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/content-graph-visualization.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Content graph visualization 3 | tags: [link, diagram, component] 4 | description: Visualize the relationships between notes as a graph 5 | --- 6 | 7 | ## Installation 8 | 9 | First, [[braindb#installation|install BrainDB]]. 10 | 11 | With BrainDB, it is possible to convert content into a graph, for example, in JSON format, and then load it into any visualization library of your choice. Here is an example of how to load data in [Graphology](https://graphology.github.io/): 12 | 13 | ```ts 14 | // src/lib/graph.ts 15 | import { BrainDB } from "@braindb/core"; 16 | import { getBrainDb } from "@braindb/astro"; 17 | import circular from "graphology-layout/circular"; 18 | import graphology from "graphology"; 19 | // @ts-ignore 20 | const { MultiGraph } = graphology; 21 | 22 | export async function toGraphologyJson(db: BrainDB) { 23 | const nodes = (await db.documents()).map((document) => ({ 24 | key: document.id(), 25 | attributes: { 26 | label: document.frontmatter().title as string, 27 | url: document.url(), 28 | size: 0.05, 29 | // color: "#f00" 30 | }, 31 | })); 32 | 33 | const edges = (await db.links()) 34 | .filter((link) => link.to() !== null) 35 | .map((link) => ({ 36 | source: link.from().id(), 37 | target: link.to()?.id(), 38 | })); 39 | 40 | return { 41 | attributes: { name: "g" }, 42 | options: { 43 | allowSelfLoops: true, 44 | multi: true, 45 | type: "directed", 46 | }, 47 | nodes, 48 | edges, 49 | }; 50 | } 51 | 52 | export async function getGraph() { 53 | const graph = new MultiGraph(); 54 | const data = await toGraphologyJson(getBrainDb()); 55 | graph.import(data as any); 56 | circular.assign(graph); 57 | return graph; 58 | } 59 | ``` 60 | 61 | ## Example 62 | 63 | See [[graph]]. 64 | 65 | This graph is generated on the server side with the help of Graphology and some custom code. It is too long to post here, so check out the source code if you want to know more. 66 | 67 | The idea was to display the graph without the need for JavaScript on the client side. However, this is challenging because one needs to manage the overlapping of nodes, edges, and labels. It is good enough to demonstrate the idea, but it needs to be reimplemented for rendering on the client side. 68 | 69 | ## See also 70 | 71 | - [[svg-pan-zoom]] 72 | 73 | ## Further improvements 74 | 75 | - [ ] Show labels for tags on the graph. 76 | - [ ] Create an example with [[graph-client]] 77 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/hasse-diagram.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hasse diagram 3 | tags: [diagram] 4 | sidebar: 5 | label: Hasse diagram 🚷 6 | description: A convenient way to visualize concepts 7 | --- 8 | 9 | ```dot 10 | digraph hasse { 11 | bgcolor="transparent"; 12 | node [shape=plaintext] 13 | edge [arrowhead=vee minlen=1]; 14 | 15 | E[label="∅"] 16 | x[label="{x}"] 17 | y[label="{y}"] 18 | z[label="{z}"] 19 | xy[label="{x, y}"] 20 | yz[label="{y, z}"] 21 | xz[label="{x, z}"] 22 | xyz[label="{x, y, z}"] 23 | 24 | E -> x 25 | E -> y 26 | E -> z 27 | x -> xy 28 | y -> xy 29 | z -> yz 30 | x -> xz 31 | y -> yz 32 | z -> xz 33 | xy -> xyz 34 | xz -> xyz 35 | yz -> xyz 36 | 37 | x -> y -> z [style=invis minlen=5] 38 | xy -> xz -> yz [style=invis minlen=5] 39 | { rank=same; y x z } 40 | { rank=same; xy yz xz } 41 | } 42 | ``` 43 | 44 | **aka**: concept lattice, poset, partially ordered set 45 | 46 | ## Theory 47 | 48 | - [Automated Lattice Drawing](https://math.hawaii.edu/~ralph/Preprints/latdrawing.pdf), 2004 49 | - [Yet a Faster Algorithm for Building the Hasse Diagram of a Concept Lattice](https://upcommons.upc.edu/bitstream/handle/2117/9034/icfca09.pdf), 2009 50 | - [Lattice Drawing: Survey of Approaches, Geometric Method, and Existing Software](https://phoenix.inf.upol.cz/~outrata/download/texts/LatDrawing-slides.pdf), 2009 51 | - [Border Algorithms for Computing Hasse Diagrams of Arbitrary Lattices](https://core.ac.uk/download/pdf/41766685.pdf), 2011 52 | - [Confluent Hasse Diagrams](https://arxiv.org/pdf/1108.5361.pdf), 2018 53 | 54 | ## References 55 | 56 | - [Hasse Diagrams of Integer Divisors](https://demonstrations.wolfram.com/HasseDiagramsOfIntegerDivisors/) 57 | 58 | ## Workaround 59 | 60 | You can create a Hasse diagram manually, for example, using [[graphviz-diagram]]. The diagram above was created with the following code: 61 | 62 | ``` 63 | digraph hasse { 64 | bgcolor="transparent"; 65 | node [shape=plaintext] 66 | edge [arrowhead=vee minlen=1]; 67 | 68 | E[label="∅"] 69 | x[label="{x}"] 70 | y[label="{y}"] 71 | z[label="{z}"] 72 | xy[label="{x, y}"] 73 | yz[label="{y, z}"] 74 | xz[label="{x, z}"] 75 | xyz[label="{x, y, z}"] 76 | 77 | E -> x 78 | E -> y 79 | E -> z 80 | x -> xy 81 | y -> xy 82 | z -> yz 83 | x -> xz 84 | y -> yz 85 | z -> xz 86 | xy -> xyz 87 | xz -> xyz 88 | yz -> xyz 89 | 90 | x -> y -> z [style=invis minlen=5] 91 | xy -> xz -> yz [style=invis minlen=5] 92 | { rank=same; y x z } 93 | { rank=same; xy yz xz } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/backlinks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Backlinks 3 | tags: [link] 4 | description: Backlinks can be useful for finding notes that reference the note you are currently reading 5 | --- 6 | 7 | ## Installation 8 | 9 | import { Steps } from "@astrojs/starlight/components"; 10 | 11 | 12 | 13 | 1. Install [[braindb#installation]] 14 | 15 | 2. Create `Backlinks` component 16 | 17 | ```astro 18 | // src/components/Backlinks.astro 19 | --- 20 | import { getBrainDb } from "@braindb/astro"; 21 | import type { CollectionEntry } from "astro:content"; 22 | interface Props { 23 | entry: CollectionEntry<"docs">; 24 | } 25 | const { entry } = Astro.props; 26 | 27 | const links = doc?.documentsFrom() 28 | .filter((doc) => doc.frontmatter().draft !== true || import.meta.env.DEV) || []; 29 | --- 30 | 31 | { 32 | links.length > 0 && ( 33 | 43 | ) 44 | } 45 | 46 | 64 | ``` 65 | 66 | 3. **Use component**, for example, in the sidebar (in Starlight) 67 | 68 | ```astro 69 | // src/components/TableOfContents.astro 70 | --- 71 | import Default from "@astrojs/starlight/components/TableOfContents.astro"; 72 | import Backlinks from "./Backlinks.astro"; 73 | --- 74 | 75 | 76 | 77 | 78 | 79 | { 80 | Astro.locals.starlightRoute.entry.data.backlinks !== false && ( 81 | 82 | ) 83 | } 84 | ``` 85 | 86 | ```js {6} 87 | // astro.config.mjs 88 | export default defineConfig({ 89 | integrations: [ 90 | starlight({ 91 | components: { 92 | TableOfContents: "./src/components/TableOfContents.astro", 93 | }, 94 | }), 95 | ], 96 | }); 97 | ``` 98 | 99 | 100 | 101 | Related: 102 | 103 | - [starlight#1335](https://github.com/withastro/starlight/discussions/1335) 104 | 105 | ## Example 106 | 107 | See an example of `` in the right sidebar 👉 108 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/braindb.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: BrainDB 3 | tags: [link] 4 | description: BrainDB is a library that allows you to treat your content as a database 5 | --- 6 | 7 | [BrainDB](https://github.com/stereobooster/braindb) is a library that allows you to treat your content as a database. It can be used to: 8 | 9 | - [[backlinks|show backlinks]] 10 | - [[wikilinks|resolve wikilinks]] 11 | - [[content-graph-visualization|visualize content as a graph]] 12 | - [[detect-broken-links|detect internal broken links]] 13 | - [[recently-changed-pages|show recently changed pages]] 14 | - [[task-extraction|extract tasks]] 15 | - [[obsidian-dataview|implement your own Obsidian Dataview]] 16 | 17 | Related: 18 | 19 | - [Markdown tools](https://stereobooster.com/posts/markdown-tools/) 20 | - [Portable markdown links](https://stereobooster.com/posts/portable-markdown-links/) 21 | 22 | ## Installation 23 | 24 | import { Steps } from "@astrojs/starlight/components"; 25 | 26 | 27 | 28 | 1. Install dependencies 29 | 30 | ```bash 31 | pnpm add @braindb/core @braindb/astro 32 | ``` 33 | 34 | 2. Configure Astro 35 | 36 | ```js {2,5} 37 | // astro.config.mjs 38 | import { brainDbAstro } from "@braindb/astro"; 39 | 40 | export default defineConfig({ 41 | integrations: [brainDbAstro()], 42 | }); 43 | ``` 44 | 45 | 46 | 47 | ## Without plugin 48 | 49 | If you need to use BrainDB without the plugin, you can do it like this: 50 | 51 | ```js 52 | // src/lib/braindb.mjs 53 | import { slug as githubSlug } from "github-slugger"; 54 | import path from "node:path"; 55 | import process from "node:process"; 56 | import { BrainDB } from "@braindb/core"; 57 | 58 | // slug implementation according to Astro 59 | // see astro/packages/astro/src/content/utils.ts 60 | function generateSlug(filePath) { 61 | const withoutFileExt = filePath.replace( 62 | new RegExp(path.extname(filePath) + "$"), 63 | "" 64 | ); 65 | const rawSlugSegments = withoutFileExt.split(path.sep); 66 | const slug = rawSlugSegments 67 | // Slugify each route segment to handle capitalization and spaces. 68 | // Note: using `slug` instead of `new Slugger()` means no slug deduping. 69 | .map((segment) => githubSlug(segment)) 70 | .join("/") 71 | .replace(/\/index$/, ""); 72 | 73 | return slug; 74 | } 75 | 76 | function slugToUrl(slug) { 77 | if (!slug.startsWith("/")) slug = "/" + slug; 78 | if (!slug.endsWith("/")) slug = slug + "/"; 79 | return slug; 80 | } 81 | 82 | const start = new Date().getTime(); 83 | 84 | export const bdb = new BrainDB({ 85 | root: path.resolve(process.cwd(), "src/content/docs"), 86 | url: (filePath, frontmatter) => { 87 | if (frontmatter.slug !== undefined) return slugToUrl(frontmatter.slug); 88 | return slugToUrl(generateSlug(filePath)); 89 | }, 90 | git: process.cwd(), 91 | }); 92 | 93 | bdb.start(); 94 | ``` 95 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/faceted-search.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Faceted search 3 | tags: [page] 4 | sidebar: 5 | label: Faceted search 🚷 6 | description: Faceted search is a method of searching through data by using "facets" (data attributes) to gradually narrow down a large data set. In the case of markdown notes, data attributes can come from front matter 7 | --- 8 | 9 | Faceted search is a method of searching through data by using "facets" (data attributes) to gradually narrow down a large data set. In the case of markdown notes, data attributes can come from front matter. 10 | 11 | ## Options 12 | 13 | ### Core 14 | 15 | - Expose full content as JSON, similar to what I did in [graph.json](https://github.com/stereobooster/astro-digital-garden/blob/main/packages/website/src/pages/graph.json.ts) + [facets](https://github.com/stereobooster/facets) 16 | - **Cons**: Not scalable for text search. 17 | - Add filters to HTML (`data-pagefind-filter`) in a hidden div + [pagefind](https://pagefind.app/docs/filtering/) 18 | - **Cons**: There are issues related to faceting. See [Discussion: Pagefind as a General Faceting Search Engine](https://github.com/CloudCannon/pagefind/discussions/512). 19 | - Hybrid solution: Use Pagefind for text search and facets library for faceting. 20 | 21 | Other less feasible options: 22 | 23 | - [SQLite WASM + HTTP Range](https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/) 24 | - I read somewhere that SQLite's text index is not optimal for this use case (that is why the author tried to use Tantivy - see next line). 25 | - [Tantivy Compiled to WASM](https://github.com/quickwit-oss/tantivy/pull/1067). 26 | 27 | ### UI 28 | 29 | - ~~[InstantSearch](https://github.com/algolia/instantsearch)~~ 30 | - ~~[Pagefind-InstantSearch](https://github.com/stereobooster/pagefind-instantsearch)~~ 31 | - Write your own simple components. 32 | 33 | ## TODO 34 | 35 | - [x] I created [a basic proof of concept using facets and InstantSearch](https://github.com/stereobooster/astro-digital-garden/tree/faceted-search-experiment). I don't like it, though. It supports facets for: 36 | - Tags 37 | - Date 38 | - [ ] I can still use `facets` or `pagefind`, but I need a different UI. 39 | - Use something slim, like Preact or Solid. However, I can't find a good components library. Or give up and use React. Then I can use `shadcn/ui` and many others. 40 | - Maybe there are options in [Components for Web](https://stereobooster.com/posts/components-for-web/). 41 | - Maybe implement a custom Pagefind UI ([@pagefind/modular-ui](https://www.npmjs.com/package/@pagefind/modular-ui)). 42 | - [ ] Other potential fields for facets: 43 | - Maybe a `stage` field (make it a separate field instead of using emojis in the title 🧠, 🚷, 🚧). 44 | - Maybe a task count (closed, open, total). 45 | - [ ] Sort by: 46 | - Date 47 | - Title 48 | - Task count 49 | 50 | ## Related 51 | 52 | - [[search-for-static-website]] 53 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/last-modified-time.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Last modified time 3 | tags: [markdown] 4 | description: Show the last modification date of the page based on Git history 5 | --- 6 | 7 | ## With BrainDB 8 | 9 | import { Steps } from "@astrojs/starlight/components"; 10 | 11 | 12 | 13 | 1. Install [[braindb#installation]] 14 | 2. Then you can access modification date, like this 15 | ```ts 16 | const doc = (await getBrainDb().documents({ slug }))[0]; 17 | doc.updatedAt(); 18 | ``` 19 | 20 | 21 | 22 | ## With Starlight 23 | 24 | Starlight already has this feature, **but** the value is not exposed in the content collection. For example, if you set: 25 | 26 | ```js {5} 27 | // astro.config.mjs 28 | export default defineConfig({ 29 | integrations: [ 30 | starlight({ 31 | lastUpdated: true, 32 | }), 33 | ], 34 | }); 35 | ``` 36 | 37 | You would see `Last updated:` on the page, but at the same time, `page.data.lastUpdated` would be `undefined`. 38 | 39 | ## With Remark plugin 40 | 41 | 42 | 43 | 1. Create remark plugin 44 | 45 | ```js 46 | // remark-modified-time.mjs 47 | import { execSync } from "child_process"; 48 | 49 | export function remarkModifiedTime() { 50 | return function (tree, file) { 51 | const filepath = file.history[0]; 52 | const result = execSync( 53 | `git log -1 --pretty="format:%cI" "${filepath}"` 54 | ); 55 | file.data.astro.frontmatter.lastUpdated = result.toString(); 56 | }; 57 | } 58 | ``` 59 | 60 | 2. Configure Astro 61 | 62 | ```js {6} 63 | // astro.config.mjs 64 | import { remarkModifiedTime } from "./remark-modified-time.mjs"; 65 | 66 | export default defineConfig({ 67 | markdown: { 68 | remarkPlugins: [remarkModifiedTime], 69 | }, 70 | }); 71 | ``` 72 | 73 | 3. You may need to adjust content schema 74 | 75 | ```ts {6} 76 | //src/content/config.ts 77 | import { z, defineCollection } from "astro:content"; 78 | 79 | const blog = defineCollection({ 80 | schema: z.object({ 81 | lastUpdated: z.string().transform((str) => new Date(str)), 82 | }), 83 | }); 84 | ``` 85 | 86 | 87 | 88 | **But** the value is accessible only after `render`: 89 | 90 | ```ts 91 | const { remarkPluginFrontmatter } = await page.render(); 92 | console.log(remarkPluginFrontmatter.lastUpdated); 93 | ``` 94 | 95 | Based on: [Astro recipes](https://docs.astro.build/en/recipes/modified-time/) 96 | 97 | ## Tips 98 | 99 | ### Github actions 100 | 101 | If you build your site with Github Actions, you need to use `fetch-depth: 0` 102 | 103 | ```yaml 104 | - uses: actions/checkout@v4 105 | with: 106 | fetch-depth: 0 107 | ``` 108 | 109 | ## Further improvements 110 | 111 | Use the last modification date (for content pages) in: 112 | 113 | - [x] [[seo-and-smo-meta-tags|schema.org microdata]] 114 | - [ ] [sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/) (`lastmod`) 115 | - [ ] in the [[sidebar]] to show "new" badge 116 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/wikilinks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wikilinks 3 | tags: [link] 4 | description: Support wiki-style links, such as [[link]], in Astro 5 | --- 6 | 7 | :::tip 8 | If you use `@braindb/astro`, it comes pre-configured with `@braindb/remark-wiki-link`. No additional actions are required. 9 | ::: 10 | 11 | ## Manual installation 12 | 13 | import { Steps } from "@astrojs/starlight/components"; 14 | 15 | 16 | 17 | 1. Install [[braindb#installation]] 18 | 2. Install `@braindb/remark-wiki-link` 19 | 20 | ```bash 21 | pnpm add @braindb/remark-wiki-link 22 | ``` 23 | 24 | 3. Configure Astro 25 | 26 | ```js 27 | // astro.config.mjs 28 | import remarkWikiLink from "@braindb/remark-wiki-link"; 29 | import { brainDbAstro, getBrainDb } from "@braindb/astro"; 30 | 31 | const bdb = getBrainDb(); 32 | await bdb.ready(); 33 | 34 | export default defineConfig({ 35 | integrations: [brainDbAstro({ remarkWikiLink: false })], 36 | markdown: { 37 | remarkPlugins: [ 38 | [ 39 | remarkWikiLink, 40 | { 41 | linkTemplate: ({ slug, alias }) => { 42 | const [slugWithoutAnchor, anchor] = slug.split("#"); 43 | if (slugWithoutAnchor) { 44 | const doc = bdb.documentsSync({ slug: slugWithoutAnchor })[0]; 45 | if (doc) { 46 | if (!doc.frontmatter().draft || import.meta.env.DEV) { 47 | return { 48 | hName: "a", 49 | hProperties: { 50 | href: anchor ? `${doc.url()}#${anchor}` : doc.url(), 51 | class: doc.frontmatter().draft ? "draft-link" : "", 52 | }, 53 | hChildren: [ 54 | { 55 | type: "text", 56 | value: 57 | alias == null ? doc.frontmatter().title : alias, 58 | }, 59 | ], 60 | }; 61 | } 62 | } 63 | } 64 | 65 | return { 66 | hName: "span", 67 | hProperties: { 68 | class: "broken-link", 69 | title: `Can't resolve link to ${slug}`, 70 | }, 71 | hChildren: [{ type: "text", value: alias || slug }], 72 | }; 73 | }, 74 | }, 75 | ], 76 | ], 77 | }, 78 | }); 79 | ``` 80 | 81 | 82 | 83 | ## Example 84 | 85 | ```md 86 | [[backlinks]] [[404|Example of broken link]] 87 | ``` 88 | 89 | [[backlinks]] [[404|Example of broken link]] 90 | 91 | ## Further Improvements 92 | 93 | - Anchors in wikilinks (`[[page#anchor]]`, `[[page#anchor|alias]]`) 94 | - Do we need to URL-encode anchors? 95 | - Do we need to slugify anchors? 96 | - [ ] Check that anchors correspond to a header in the target document. 97 | - [ ] What about ambiguous links (`bdb.documentsSync({ slug: permalink }).length > 1`)? 98 | - [ ] Image wikilinks (`![[some.jpg]]`) 99 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/metro-map-diagram.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Metro map diagram 3 | tags: [component, diagram] 4 | sidebar: 5 | label: Metro map diagram 🚷 6 | description: Also known as a transit map, subway map, bus stop map, schematic map, railroad map, or network map 7 | --- 8 | 9 | ![Lisboa Subway Map](./metro-map-diagram/lisboa.output.svg) 10 | 11 | Example taken from [here](https://github.com/juliuste/transit-map/). 12 | 13 | **aka**: Transit map, subway map, bus stop map, schematic map, railroad map, network map. 14 | 15 | ## Theory 16 | 17 | - [Automated Drawing of Metro Maps](https://i11www.iti.kit.edu/extra/publications/n-admm-05da.pdf), 2005 18 | - [Order in the Underground – How to Automate the Drawing of Metro Maps](https://www1.pub.informatik.uni-wuerzburg.de/pub/wolff/slides/nw-mipdh-06-slides.pdf), 2005 19 | - [Automatic Metro Map Design Techniques](https://icaci.org/files/documents/ICC_proceedings/ICC2005/htm/pdf/oral/TEMA3/Session%205/JONATHAN%20M.%20STOTT.pdf), 2005 20 | - [Automatic Metro Map Layout Using Multicriteria Optimization](https://core.ac.uk/download/pdf/10635852.pdf), 2007 21 | - [Drawing Metro Maps Using Bézier Curves](https://link.springer.com/chapter/10.1007/978-3-642-36763-2_41), 2012 22 | - [Drawing and Labeling High-Quality Metro Maps by Mixed-Integer Programming](https://www1.pub.informatik.uni-wuerzburg.de/pub/wolff/pub/nw-dlhqm-10.pdf), 2010 23 | - [New Algorithm for Automatic Visualization of Metro Map](https://ijcsi.org/papers/IJCSI-10-4-2-225-229.pdf), 2013 24 | - [Octilinear Force-Directed Layout with Mental Map Preservation for Schematic Diagrams](https://core.ac.uk/download/pdf/20523942.pdf) 25 | - [Automatic Drawing for Tokyo Metro Map](https://conference.imp.fu-berlin.de/eurocg18/download/paper_62.pdf), 2018 26 | - [Drawing k-linear Metro Maps](https://www.ac.tuwien.ac.at/files/pub/smw19-paper-6.pdf), 2019 27 | - [Real-time Linear Cartograms and Metro Maps](https://github.com/tcvdijk/papers/blob/master/conference/sigspatial18_realtime_linear_cartograms.pdf), 2018 28 | - [Code](https://github.com/tcvdijk/fast-linear-carto) 29 | - [The State of the Art in Map-Like Visualization](https://www.researchgate.net/publication/343051883_The_State_of_the_Art_in_Map-Like_Visualization), 2020 30 | - [Metro Maps on Octilinear Grid Graphs](https://www.researchgate.net/publication/343051484_Metro_Maps_on_Octilinear_Grid_Graphs) 31 | - [Shape-Guided Mixed Metro Map Layout](https://arxiv.org/pdf/2208.14261.pdf), 2022 32 | - [A Survey on Computing Schematic Network Maps: The Challenge to Interactivity](https://arxiv.org/pdf/2208.07301.pdf), 2022 33 | - [MetroSAT: Logic-based Computation of Metro Maps](https://www.ruhr-uni-bochum.de/schematicmapping/papers/smw-fuchs-nickel-noellenburg.pdf), 2022 34 | - [Video](https://www.youtube.com/watch?v=2pKooKSgc-Q) 35 | 36 | ### Related 37 | 38 | - [MetroSets: Visualizing Sets as Metro Maps](https://arxiv.org/abs/2008.09367), 2020 39 | 40 | ## Code 41 | 42 | - [transit-map](https://github.com/juliuste/transit-map) 43 | - [automatic-metro-map](https://github.com/gipong/automatic-metro-map) 44 | - [OpenMetroMaps](https://github.com/OpenMetroMaps/OpenMetroMaps) 45 | 46 | ## References 47 | 48 | - [edwardtufte.com](https://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=00005W) 49 | - [transitmap.net](https://transitmap.net/) 50 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/icons-to-external-links.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Icons for external links 3 | tags: [markdown] 4 | description: Visually differentiate between external and internal links. 5 | --- 6 | 7 | ## Arrows for external links 8 | 9 | ### Installation 10 | 11 | import { Steps } from "@astrojs/starlight/components"; 12 | 13 | 14 | 15 | 1. Install dependencies 16 | 17 | ```bash 18 | pnpm add rehype-external-links 19 | ``` 20 | 21 | 2. Configure Astro 22 | 23 | ```js {2, 7, 12-18} 24 | // astro.config.mjs 25 | import rehypeExternalLinks from "rehype-external-links"; 26 | 27 | export default defineConfig({ 28 | integrations: [ 29 | starlight({ 30 | customCss: ["./src/styles/custom.css"], 31 | }), 32 | ], 33 | markdown: { 34 | rehypePlugins: [ 35 | [ 36 | rehypeExternalLinks, 37 | { 38 | content: { type: "text", value: " ↗" }, // ⤴ 39 | contentProperties: { "aria-hidden": true, class: "no-select" }, 40 | }, 41 | ], 42 | ], 43 | }, 44 | }); 45 | ``` 46 | 47 | 3. Add CSS 48 | 49 | ```css 50 | // src/styles/custom.css 51 | .no-select { 52 | user-select: none; 53 | } 54 | ``` 55 | 56 | 57 | 58 | Based on: [Astro recipes](https://docs.astro.build/en/recipes/external-links/) 59 | 60 | ### Example 61 | 62 | ```md 63 | // example.md 64 | https://example.com 65 | ``` 66 | 67 | https://example.com 68 | 69 | ## Favicons for external links 70 | 71 | ### Installation 72 | 73 | 74 | 75 | 1. Install dependencies 76 | 77 | ```bash 78 | pnpm add rehype-external-links 79 | ``` 80 | 81 | 2. Configure Astro 82 | 83 | ```js {2, 7, 12-24} 84 | // astro.config.mjs 85 | import rehypeExternalLinks from "rehype-external-links"; 86 | 87 | export default defineConfig({ 88 | integrations: [ 89 | starlight({ 90 | customCss: ["./src/styles/custom.css"], 91 | }), 92 | ], 93 | markdown: { 94 | rehypePlugins: [ 95 | [ 96 | rehypeExternalLinks, 97 | { 98 | content: { type: "text", value: "" }, 99 | contentProperties: (x) => { 100 | const hostname = new URL(x.properties.href).hostname; 101 | return { 102 | class: "external-icon", 103 | style: `--icon: url(https://external-content.duckduckgo.com/ip3/${hostname}.ico)`, 104 | }; 105 | }, 106 | }, 107 | ], 108 | ], 109 | }, 110 | }); 111 | ``` 112 | 113 | 3. Add CSS 114 | 115 | ```css 116 | // src/styles/custom.css 117 | .external-icon { 118 | background-image: var(--icon); 119 | background-color: #fff; 120 | background-size: cover; 121 | color: transparent; 122 | padding-left: 1.2rem; 123 | border-radius: 0.2rem; 124 | margin-left: 0.2rem; 125 | } 126 | ``` 127 | 128 | 129 | 130 | ### Example 131 | 132 | ```md 133 | // example.md 134 | https://stereobooster.com 135 | ``` 136 | 137 | `https://stereobooster.com` 138 | 139 | ### TODO 140 | 141 | - [ ] add other [favicon providers](https://docs.searxng.org/_modules/searx/favicons/resolvers.html) -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/timeline-diagram.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Timelime diagram 3 | tags: [component, diagram] 4 | description: Astro component to visualize timelines or genealogical trees 5 | --- 6 | 7 | 8 | 9 | Example taken [here](https://exact.stereobooster.com/). 10 | 11 | **aka** chronology, genealogical tree, layered digraph. 12 | 13 | ## Installation 14 | 15 | **TODO**: 16 | 17 | - [ ] Remove the `Graphviz` component from the repo; need to use `@beoe/astro-graphviz` instead. 18 | 19 | First, install [[graphviz-diagram|Graphviz]]: 20 | 21 | ```astro 22 | // src/components/Timeline.astro 23 | --- 24 | import Graphviz from "./Graphviz.astro"; 25 | 26 | type TimelineItem = { 27 | id: string; 28 | year: number; 29 | tooltip?: string; 30 | class?: string; 31 | label?: string; 32 | url?: string; 33 | in?: string[]; 34 | out?: string[]; 35 | }; 36 | 37 | interface Props { 38 | items: TimelineItem[]; 39 | /** 40 | * https://www.graphviz.org/doc/info/attrs.html#d:rankdir 41 | */ 42 | direction?: "TB" | "BT" | "LR" | "RL"; 43 | } 44 | 45 | const { items, direction } = Astro.props; 46 | 47 | const byYears: Record = {}; 48 | 49 | items.forEach((item) => { 50 | byYears[item.year] = byYears[item.year] || []; 51 | byYears[item.year].push(item.id); 52 | }); 53 | 54 | const src = `digraph timeline { 55 | ${direction ? `rankdir=${direction}` : ""} 56 | bgcolor="transparent"; 57 | size="7,8"; 58 | 59 | edge [style=invis]; 60 | node [fontsize=24, shape = plaintext]; 61 | 62 | ${Object.keys(byYears).sort().join(` -> `)} 63 | 0[label=" "] 64 | 65 | node [fontsize=20, shape = box]; 66 | 67 | ${Object.keys(byYears) 68 | .sort() 69 | .map( 70 | (year) => 71 | `{ rank=same; "${year}" ${byYears[year].map((x) => `"${x}"`).join(" ")}; }` 72 | ) 73 | .join("\n")} 74 | 75 | edge[style=solid]; 76 | 77 | ${items.map((item) => `"${item.id}"[${item.url ? `URL="${item.url}"` : ""} ${item.label ? `label="${item.label}"` : ""} ${item.class ? `class="${item.class}"` : ""} ${item.tooltip ? `tooltip="${item.tooltip}"` : ""}];`).join("\n")} 78 | 79 | ${items.map((item) => (!item.in ? "" : item.in.map((id) => `"${id}" -> "${item.id}";`).join("\n"))).join("\n")} 80 | 81 | ${items.map((item) => (!item.out ? "" : item.out.map((id) => `"${item.id}" -> "${id}";`).join("\n"))).join("\n")} 82 | }`; 83 | --- 84 | 85 |
86 | 87 |
88 | 89 | 102 | ``` 103 | 104 | ## Example 105 | 106 | Code: [Publications.astro](https://github.com/stereobooster/exact-online-string-matching/blob/main/src/components/Publications.astro). Online demo: [Exact Online String Matching](https://exact.stereobooster.com/). 107 | 108 | ## References 109 | 110 | - [Timeline of Web Browsers](https://upload.wikimedia.org/wikipedia/commons/7/74/Timeline_of_web_browsers.svg) 111 | - [Timelines Chart](https://github.com/vasturiano/timelines-chart) 112 | - [Timelines and Visual Histories](http://euclid.psych.yorku.ca/SCS/Gallery/timelines.html) 113 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/seo-and-smo-meta-tags.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: SEO and SMO meta tags 3 | tags: [component] 4 | description: How to add SEO (search engine optimization) and SMO (social media optimization) tags to an Astro-based website 5 | --- 6 | 7 | ## Astro Plugins 8 | 9 | These are the most popular plugins for the task: 10 | 11 | - [astro-seo](https://github.com/jonasmerlin/astro-seo): This Astro component makes it easy to add tags relevant for search engine optimization (SEO) to your pages. 12 | - [@astrolib/seo](https://github.com/onwidget/astrolib/tree/main/packages/seo): Astro SEO is an integration that makes managing your SEO easier in Astro projects. It is fully based on the excellent Next SEO library. 13 | - [astro-seo-meta](https://github.com/codiume/orbit/tree/main/packages/astro-seo-meta): Astro SEO provides an SEO component to update meta tags. 14 | - [astro-seo-schema](https://github.com/codiume/orbit/tree/main/packages/astro-seo-schema): Easily insert valid Schema.org JSON-LD in your Astro apps. 15 | 16 | ## Metadata 17 | 18 | :::caution 19 | This is an experiment. I need to investigate its impact. 20 | ::: 21 | 22 | ### type: NewsArticle 23 | 24 | import { Steps } from "@astrojs/starlight/components"; 25 | 26 | 27 | 28 | 1. Install dependencies 29 | 30 | ```bash 31 | pnpm add schema-dts astro-seo-schema 32 | ``` 33 | 34 | 2. Add `Schema` to `Head` 35 | 36 | ```astro {10-24} 37 | // src/components/Head.astro 38 | --- 39 | import Default from "@astrojs/starlight/components/Head.astro"; 40 | import { Schema } from "astro-seo-schema"; 41 | --- 42 | 43 | 44 | 45 | 60 | ``` 61 | 62 | 3. Change Astro config 63 | 64 | ```js {6, 8} 65 | // astro.config.mjs 66 | export default defineConfig({ 67 | integrations: [ 68 | starlight({ 69 | components: { 70 | Head: "./src/components/Head.astro", 71 | }, 72 | lastUpdated: true, 73 | }), 74 | ], 75 | }); 76 | ``` 77 | 78 | 79 | 80 | ### type: Article 81 | 82 | Try `"@type":"Article"` with the following elements: 83 | 84 | - `
` 85 | - `` 86 | - `

...

` 87 | - `
...
` 88 | 89 | ## Sites to Test Meta Tags 90 | 91 | - [Google Rich Results Test](https://search.google.com/test/rich-results) 92 | - [Schema.org Validator](https://validator.schema.org/) 93 | 94 | ## TODO 95 | 96 | - [ ] Plugin that places sitemap URL in `robots.txt` (`astro-robots-txt`) 97 | - [ ] Use the "updated at" date in the sitemap (so search engines would rescan those pages first) 98 | - Maybe I can use BrainDb to generate it? 99 | - [ ] Use LLM to generate descriptions 100 | ```js 101 | import { pipeline } from "@huggingface/transformers"; 102 | const summarizer = await pipeline("summarization"); 103 | const summary = await summarizer("Some text here"); 104 | ``` 105 | -------------------------------------------------------------------------------- /packages/website/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | import { starlightKatex } from "starlight-katex"; 4 | import Icons from "unplugin-icons/vite"; 5 | 6 | import { getCache } from "@beoe/cache"; 7 | import { rehypeMermaid } from "@beoe/rehype-mermaid"; 8 | import { rehypeGraphviz } from "@beoe/rehype-graphviz"; 9 | import { rehypeGnuplot } from "@beoe/rehype-gnuplot"; 10 | 11 | import starlightDigitalGarden from "starlight-digital-garden"; 12 | 13 | const cache = await getCache(); 14 | const diagramConfigs = { 15 | cache, 16 | strategy: "file", 17 | darkScheme: "class", 18 | fsPath: "public/beoe", 19 | webPath: "/beoe", 20 | }; 21 | 22 | // https://astro.build/config 23 | export default defineConfig({ 24 | site: "https://astro-digital-garden.stereobooster.com", 25 | integrations: [ 26 | starlight({ 27 | title: "Astro Digital Garden", 28 | social: [ 29 | { 30 | icon: "github", 31 | label: "GitHub", 32 | href: "https://github.com/stereobooster/astro-digital-garden", 33 | }, 34 | ], 35 | editLink: { 36 | baseUrl: 37 | "https://github.com/stereobooster/astro-digital-garden/edit/main/packages/website/", 38 | }, 39 | lastUpdated: true, 40 | sidebar: [ 41 | { label: "Introduction", link: "/" }, 42 | { 43 | label: "Recipes", 44 | // collapsed: true, 45 | autogenerate: { 46 | directory: "recipes", 47 | }, 48 | }, 49 | ], 50 | customCss: ["./src/styles/custom.css"], 51 | components: { 52 | TableOfContents: "./src/components/TableOfContents.astro", 53 | Head: "./src/components/Head.astro", 54 | // Sidebar: "./src/components/Sidebar.astro", 55 | }, 56 | // If you want to fork this repository for personal use, 57 | // please remove following lines for analytics 58 | head: import.meta.env.PROD 59 | ? [ 60 | { 61 | tag: "script", 62 | attrs: { 63 | src: "https://eu.umami.is/script.js", 64 | "data-website-id": "2d34b0d4-893c-4348-a3e4-1f489300117c", 65 | defer: true, 66 | }, 67 | }, 68 | { 69 | tag: "script", 70 | attrs: { 71 | src: "https://beampipe.io/js/tracker.js", 72 | "data-beampipe-domain": 73 | "astro-digital-garden.stereobooster.com", 74 | defer: true, 75 | async: true, 76 | }, 77 | }, 78 | ] 79 | : undefined, 80 | plugins: [ 81 | starlightKatex(), 82 | starlightDigitalGarden({ remarkDataview: true }), 83 | ], 84 | }), 85 | ], 86 | markdown: { 87 | remarkRehype: { 88 | // https://github.com/remarkjs/remark-rehype?tab=readme-ov-file#options 89 | footnoteBackContent: "⤴", 90 | // footnoteLabel: "Footnotes", 91 | // footnoteLabelTagName: "h2", 92 | }, 93 | // remarkPlugins: [], 94 | rehypePlugins: [ 95 | [rehypeMermaid, diagramConfigs], 96 | [rehypeGnuplot, diagramConfigs], 97 | [rehypeGraphviz, { class: "not-content", cache }], 98 | ], 99 | }, 100 | vite: { 101 | plugins: [ 102 | Icons({ 103 | compiler: "astro", 104 | }), 105 | ], 106 | }, 107 | }); 108 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/graphviz-diagram.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Graphviz diagram 3 | tags: [markdown, code-fences, diagram] 4 | description: Graphviz inside "code fences" converted to diagrams - static SVGs, no client-side JavaScript 5 | --- 6 | 7 | ```dot alt="example of the Graphviz diagram" 8 | digraph finite_state_machine { 9 | bgcolor="transparent"; 10 | fontname="Helvetica,Arial,sans-serif"; 11 | node [fontname="Helvetica,Arial,sans-serif"] 12 | edge [fontname="Helvetica,Arial,sans-serif"] 13 | rankdir=LR; 14 | node [shape = doublecircle]; 0 3 4 8; 15 | node [shape = circle]; 16 | 0 -> 2 [label = "SS(B)"]; 17 | 0 -> 1 [label = "SS(S)"]; 18 | 1 -> 3 [label = "S($end)"]; 19 | 2 -> 6 [label = "SS(b)"]; 20 | 2 -> 5 [label = "SS(a)"]; 21 | 2 -> 4 [label = "S(A)"]; 22 | 5 -> 7 [label = "S(b)"]; 23 | 5 -> 5 [label = "S(a)"]; 24 | 6 -> 6 [label = "S(b)"]; 25 | 6 -> 5 [label = "S(a)"]; 26 | 7 -> 8 [label = "S(b)"]; 27 | 7 -> 5 [label = "S(a)"]; 28 | 8 -> 6 [label = "S(b)"]; 29 | 8 -> 5 [label = "S(a)"]; 30 | } 31 | ``` 32 | 33 | Example taken [here](https://graphviz.org/Gallery/directed/fsm.html) 34 | 35 | ## About 36 | 37 | [Graphviz](https://graphviz.org/) is open-source graph visualization software. On one hand, it is a general diagramming tool, so it doesn't have a DSL to directly express specific diagrams (let's say a Hasse diagram or an ER diagram). On the other hand, if you don't have a better solution, you can always bend Graphviz to create the desired diagram (though it can be tedious sometimes). 38 | 39 | ## Installation 40 | 41 | import { Steps } from "@astrojs/starlight/components"; 42 | 43 | 44 | 1. Install dependencies 45 | 46 | ```bash 47 | pnpm add @beoe/rehype-graphviz 48 | ``` 49 | 50 | 2. Configure Astro. See note about [[rehype-plugins-for-code]]. 51 | 52 | ```js {2,6} 53 | // astro.config.mjs 54 | import { rehypeGraphviz } from "@beoe/rehype-graphviz"; 55 | 56 | export default defineConfig({ 57 | markdown: { 58 | rehypePlugins: [[rehypeGraphviz, { class: "not-content" }]], 59 | }, 60 | }); 61 | ``` 62 | 63 | 3. **Optional** install dependency for cache 64 | 65 | ```bash 66 | pnpm add @beoe/cache 67 | ``` 68 | 69 | 4. **Optional** configure cache 70 | 71 | ```js {2, 4, 8} 72 | // astro.config.mjs 73 | import { getCache } from "@beoe/cache"; 74 | 75 | const cache = await getCache(); 76 | 77 | export default defineConfig({ 78 | markdown: { 79 | rehypePlugins: [[rehypeGraphviz, { class: "not-content", cache }]], 80 | }, 81 | }); 82 | ``` 83 | 84 | 5. **Optional** add [[svg-pan-zoom#installation|pan and zoom for diagrams]] 85 | 86 | 87 | 88 | ## Tips 89 | 90 | ### Dark mode 91 | 92 | Basic dark mode can be implemented with: 93 | 94 | ```css 95 | .graphviz { 96 | text { 97 | fill: var(--sl-color-white); 98 | } 99 | [fill="black"], 100 | [fill="#000"] { 101 | fill: var(--sl-color-white); 102 | } 103 | [stroke="black"], 104 | [stroke="#000"] { 105 | stroke: var(--sl-color-white); 106 | } 107 | } 108 | ``` 109 | 110 | Plus, you can pass [`class`](https://graphviz.org/docs/attrs/class/) to edges and nodes to implement **advanced dark mode**. 111 | 112 | ### To remove background 113 | 114 | To remove the background, use `bgcolor="transparent"`. 115 | 116 | ## Example 117 | 118 | ````md 119 | ```dot alt="example of the Graphviz diagram" 120 | digraph x {bgcolor="transparent";rankdir=LR;node [shape=box] 121 | Start -> Stop} 122 | ``` 123 | ```` 124 | 125 | ```dot alt="example of the Graphviz diagram" 126 | digraph x {bgcolor="transparent";rankdir=LR;node [shape=box] 127 | Start -> Stop} 128 | ``` 129 | 130 | Compare it to [[mermaid-diagrams-in-markdown#example|similar example in Mermaid]]. 131 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/anchors-for-headings.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Anchors for headings 3 | tags: [markdown] 4 | description: Add a way to link directly to headings in the text and easily copy those links, similar to GitHub 5 | --- 6 | 7 | ## Introduction 8 | 9 | There are different opinions on the best approach for these links. See: 10 | 11 | - [markdown-it-anchor#82](https://github.com/valeriangalliat/markdown-it-anchor/issues/82#issuecomment-788268457) 12 | - [Are Your Anchor Links Accessible?](https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/) 13 | 14 | ## Installation 15 | 16 | ### Anchor before 17 | 18 | I prefer when an anchor is placed before the heading (like in GitHub), but it doesn't work well with the Starlight design. 19 | 20 | import { Steps } from "@astrojs/starlight/components"; 21 | 22 | 23 | 1. Install dependencies 24 | 25 | ```bash 26 | pnpm add rehype-autolink-headings @astrojs/markdown-remark 27 | ``` 28 | 29 | 2. Configure Astro 30 | 31 | ```js {2-3, 8, 12} 32 | // astro.config.mjs 33 | import { rehypeHeadingIds } from "@astrojs/markdown-remark"; 34 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 35 | 36 | export default defineConfig({ 37 | integrations: [ 38 | starlight({ 39 | customCss: ["./src/styles/custom.css"], 40 | }), 41 | ], 42 | markdown: { 43 | rehypePlugins: [rehypeHeadingIds, rehypeAutolinkHeadings], 44 | }, 45 | }); 46 | ``` 47 | 48 | 3. Add CSS 49 | 50 | ```css 51 | // src/styles/custom.css 52 | .sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { 53 | a { 54 | color: var(--sl-color-black); 55 | text-decoration: none; 56 | font-size: 1.5rem; 57 | margin-left: calc(-1rem - 4px); 58 | padding-right: 4px; 59 | } 60 | 61 | &:hover a { 62 | color: var(--sl-color-accent); 63 | } 64 | 65 | .icon.icon-link::after { 66 | content: "#"; 67 | } 68 | } 69 | ``` 70 | 71 | 72 | 73 | ### Anchor after 74 | 75 | 76 | 1. Install dependencies 77 | 78 | ```bash 79 | pnpm add rehype-autolink-headings @astrojs/markdown-remark 80 | ``` 81 | 82 | 2. Configure Astro 83 | 84 | ```js {2-3, 8, 13-14} 85 | // astro.config.mjs 86 | import { rehypeHeadingIds } from "@astrojs/markdown-remark"; 87 | import rehypeAutolinkHeadings from "rehype-autolink-headings"; 88 | 89 | export default defineConfig({ 90 | integrations: [ 91 | starlight({ 92 | customCss: ["./src/styles/custom.css"], 93 | }), 94 | ], 95 | markdown: { 96 | rehypePlugins: [ 97 | rehypeHeadingIds, 98 | [rehypeAutolinkHeadings, { behavior: "append" }], 99 | ], 100 | }, 101 | }); 102 | ``` 103 | 104 | 3. Add CSS 105 | 106 | ```css 107 | // src/styles/custom.css 108 | .sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { 109 | a { 110 | color: var(--sl-color-black); 111 | text-decoration: none; 112 | font-size: 1.5rem; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | &:hover a { 117 | color: var(--sl-color-accent); 118 | } 119 | 120 | .icon.icon-link::after { 121 | content: "#"; 122 | } 123 | } 124 | ``` 125 | 126 | 127 | 128 | ## Starlight 129 | 130 | Starlight 0.34.0+ added support for generating clickable anchor links for headings. 131 | 132 | ## See also 133 | 134 | - [Anchor Links for Headings](https://github.com/withastro/starlight/discussions/1239) 135 | - [Add links to Starlight headings](https://hideoo.dev/notes/starlight-heading-links) 136 | - [withastro#5610](https://github.com/withastro/docs/pull/5610/files) 137 | - [Support custom header IDs in markdown](https://github.com/withastro/roadmap/discussions/329) 138 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/task-extraction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tasks extraction 3 | tags: [component, gfm] 4 | description: How to extract tasks from Markdown files, like they do in GitHub issues 5 | --- 6 | 7 | **aka** Checklist, TODO 8 | 9 | ## Introduction 10 | 11 | Imagine you are writing an article and have a checklist of what else you want to cover in the article. 12 | 13 | ```md 14 | - [ ] write about ... 15 | - [ ] check that this is correct assumption ... 16 | - [ ] add citation 17 | 18 | # My article 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 21 | ``` 22 | 23 | On one hand, it is convenient to keep this checklist in the article itself. On the other hand, you may want to see all tasks at a glance. With the help of BrainDB, it is possible to extract all tasks from all pages and list them on one page. 24 | 25 | ## Installation 26 | 27 | import { Steps } from "@astrojs/starlight/components"; 28 | 29 | 30 | 31 | 1. Install [[braindb#installation]] 32 | 33 | 2. Create `TaskList` component 34 | 35 | ```astro 36 | // src/components/TaskList.astro 37 | --- 38 | import type { Task } from "@braindb/core"; 39 | import { getBrainDb } from "@braindb/astro"; 40 | 41 | // TODO: shall I sort pages by title (alphabetically) or by date (recent first)? 42 | const grouped: Record = {}; 43 | (await getBrainDb().tasks()).forEach((task) => { 44 | const doc = task.from(); 45 | if (!doc.frontmatter().draft || import.meta.env.DEV) { 46 | const path = doc.path(); 47 | grouped[path] = grouped[path] || []; 48 | grouped[path].push(task); 49 | } 50 | }); 51 | --- 52 | 53 | { 54 | Object.values(grouped).map((tasks) => ( 55 |

56 | {tasks[0].from().title()} 57 |

    58 | {tasks.map((task) => ( 59 |
  • 60 | 68 |
  • 69 | ))} 70 |
71 |

72 | )) 73 | } 74 | ``` 75 | 76 | 3. **Use component**, wherever you like 77 | 78 | ```mdx 79 | --- 80 | title: Tasks 81 | tableOfContents: false 82 | prev: false 83 | next: false 84 | --- 85 | 86 | import TaskList from "@components/TaskList.astro"; 87 | 88 | 89 | ``` 90 | 91 |
92 | 93 | ## Limitations 94 | 95 | For now, I have implemented the bare minimum for task extraction in BrainDB. These are the limitations that I want to address in the future. 96 | 97 | ### Limitation 1: flat list 98 | 99 | ```md 100 | - [ ] task 101 | - [x] sub-task 102 | ``` 103 | 104 | BrainDB will return this as a flat list instead of a hierarchical structure: 105 | 106 | ```md 107 | - [ ] task 108 | - [x] sub-task 109 | ``` 110 | 111 | ### Limitation 2: text only 112 | 113 | ```md 114 | - [ ] `code` **bold** 115 | ``` 116 | 117 | BrainDB can return Markdown for the content of the task, but for now, I haven't figured out a good way to render it with Astro. I use plain text instead: 118 | 119 | ```md 120 | - [ ] code bold 121 | ``` 122 | 123 | ### Limitation 3: no subitems 124 | 125 | ```md 126 | - [ ] task 127 | - a 128 | - b 129 | ``` 130 | 131 | For now, BrainDB returns only the content of the task, but not the subitems: 132 | 133 | ```md 134 | - [ ] task 135 | ``` 136 | 137 | ## Example 138 | 139 | This is an **example** of `` output 👇 140 | 141 | ```dataview list 142 | SELECT dv_link(), dv_task() 143 | FROM tasks JOIN documents ON documents.path = tasks.from 144 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 145 | AND url != '/tasks/' 146 | AND checked = false 147 | ORDER BY updated_at DESC, path, tasks.start; 148 | ``` 149 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/svg-pan-zoom.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Pan and zoom for images 3 | tags: [component] 4 | description: Pan and zoom for images with gesture support. Useful for SVG images and diagrams 5 | --- 6 | 7 | ```gnuplot 8 | plot [-20:10] sin(x) 9 | ``` 10 | 11 | **Try to use gestures with this image 👆** 12 | 13 | ## Introduction 14 | 15 | [`@beoe/pan-zoom`](https://github.com/stereobooster/beoe/tree/main/packages/pan-zoom) is a small client-side script that adds pan and zoom capabilities to SVG images (or any DOM node, actually). 16 | 17 | There are many similar scripts, but the main difference is that this one **supports gestures for all types of devices**: 18 | 19 | | Intention | Mouse | Trackpad/Touchpad | Touchscreen | 20 | | --------- | ----------------------- | ----------------- | --------------- | 21 | | Pan | Click + move | Click + move | Two-finger drag | 22 | | Zoom | Ctrl + wheel | Pinch | Pinch | 23 | | Reset | Double click | Double click | Double tap | 24 | | | | | | 25 | | Scroll | Wheel | Two-finger drag | One-finger drag | 26 | 27 | Pay attention to the following: 28 | 29 | - Gestures are intentionally selected to avoid interfering with the system's default scroll gestures, **to prevent "scroll traps."** 30 | - Cmd + click - zoom in. 31 | - Alt + click - zoom out. 32 | - The first double click (or tap) - zooms in by 2x. 33 | 34 | ## Installation 35 | 36 | import { Steps } from "@astrojs/starlight/components"; 37 | 38 | 39 | 40 | 1. Install dependencies 41 | 42 | ```bash 43 | pnpm add @beoe/pan-zoom 44 | ``` 45 | 46 | 2. Add `svgpanzoom.ts` 47 | 48 | ```ts 49 | // src/components/svgpanzoom.ts 50 | import "@beoe/pan-zoom/css/PanZoomUi.css"; 51 | import { PanZoomUi } from "@beoe/pan-zoom"; 52 | 53 | // for BEOE diagrams 54 | document.querySelectorAll(".beoe").forEach((container) => { 55 | const element = container.firstElementChild; 56 | if (!element) return; 57 | // @ts-expect-error 58 | new PanZoomUi({ element, container }).on(); 59 | }); 60 | 61 | // for content images 62 | document 63 | .querySelectorAll( 64 | ".sl-markdown-content > img[src$='.svg' i]," + 65 | ".sl-markdown-content > p > img[src$='.svg' i]," + 66 | // for development environment 67 | ".sl-markdown-content > img[src$='f=svg' i]," + 68 | ".sl-markdown-content > img[src$='f=svg' i]" 69 | ) 70 | .forEach((element) => { 71 | if (element.parentElement?.tagName === "PICTURE") { 72 | element = element.parentElement; 73 | } 74 | const container = document.createElement("figure"); 75 | container.classList.add("beoe", "not-content"); 76 | element.replaceWith(container); 77 | container.append(element); 78 | // @ts-expect-error 79 | new PanZoomUi({ element, container }).on(); 80 | }); 81 | ``` 82 | 83 | 3. Use `svgpanzoom.ts` in the base layout 84 | 85 | 86 | 87 | ### Starlight specific code 88 | 89 | 90 | 91 | 1. Use `svgpanzoom.ts` in the `PageFrame` 92 | 93 | ```astro {13-15} 94 | // src/components/PageFrame.astro 95 | --- 96 | import Default from "@astrojs/starlight/components/PageFrame.astro"; 97 | --- 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 108 | ``` 109 | 110 | 2. Override `PageFrame` in Astro config 111 | 112 | ```js {6} 113 | // astro.config.mjs 114 | export default defineConfig({ 115 | integrations: [ 116 | starlight({ 117 | components: { 118 | PageFrame: "./src/components/PageFrame.astro", 119 | }, 120 | }), 121 | ], 122 | }); 123 | ``` 124 | 125 | 126 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/tag-list.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Tag list 3 | tags: [page] 4 | sidebar: 5 | label: Tag list 🚧 6 | description: Astro component to show notes grouped by tags 7 | --- 8 | 9 | ## Implementation in Starlight 10 | 11 | import { Steps } from "@astrojs/starlight/components"; 12 | 13 | 14 | 15 | 1. Create `TagList` component 16 | 17 | ```astro 18 | //src/components/TagList.astro 19 | --- 20 | import { getCollection } from "astro:content"; 21 | 22 | const docs = await getCollection("docs"); 23 | type Docs = typeof docs; 24 | const docsByTags = new Map(); 25 | docs.forEach((doc) => { 26 | if (doc.data.tags) { 27 | doc.data.tags.forEach((tag: string) => { 28 | docsByTags.set(tag, docsByTags.get(tag) || []); 29 | docsByTags.get(tag)?.push(doc); 30 | }); 31 | } 32 | }); 33 | const comparator = new Intl.Collator("en"); 34 | const tagsSorted = [...docsByTags.keys()].sort(comparator.compare); 35 | --- 36 | 37 |
    38 | { 39 | tagsSorted.map((tag) => ( 40 |
  • 41 | {tag} ({docsByTags.get(tag)?.length}) 42 |
      43 | {docsByTags.get(tag)?.map((doc) => ( 44 |
    • 45 | {doc.data.title} 46 |
    • 47 | ))} 48 |
    49 |
  • 50 | )) 51 | } 52 |
53 | ``` 54 | 55 | 2. Adjust content schema 56 | 57 | ```ts {8-10} 58 | //src/content/config.ts 59 | import { z, defineCollection } from "astro:content"; 60 | import { docsSchema, i18nSchema } from "@astrojs/starlight/schema"; 61 | 62 | export const collections = { 63 | docs: defineCollection({ 64 | schema: docsSchema({ 65 | extend: z.object({ 66 | tags: z.array(z.string()).optional(), 67 | }), 68 | }), 69 | }), 70 | i18n: defineCollection({ type: "data", schema: i18nSchema() }), 71 | }; 72 | ``` 73 | 74 | 3. **Use component**, wherever you like 75 | 76 | ```mdx 77 | --- 78 | title: Tags 79 | tableOfContents: false 80 | prev: false 81 | next: false 82 | --- 83 | 84 | import TagList from "@components/TagList.astro"; 85 | 86 | 87 | ``` 88 | 89 |
90 | 91 | ## Example 92 | 93 | See [[tags]]. 94 | 95 | ## Further improvements 96 | 97 | - [ ] Page for each tag 98 | - [ ] Metadata for tags (color, icon) 99 | 100 | ### Tag page 101 | 102 | From [starlight-blog](https://github.com/HiDeoo/starlight-blog): 103 | 104 | - [Tag Page](https://github.com/HiDeoo/starlight-blog/blob/main/packages/starlight-blog/routes/Tags.astro) 105 | - [Tag-Related Functions](https://github.com/HiDeoo/starlight-blog/blob/main/packages/starlight-blog/libs/tags.ts) 106 | - [`injectRoute` Example](https://github.com/HiDeoo/starlight-blog/blob/main/packages/starlight-blog/index.ts#L29-L33) 107 | ```ts 108 | addIntegration({ 109 | name: "starlight-blog-integration", 110 | hooks: { 111 | "astro:config:setup": ({ injectRoute, updateConfig }) => { 112 | injectRoute({ 113 | entrypoint: "starlight-blog/routes/Tags.astro", 114 | pattern: "/blog/tags/[tag]", 115 | prerender: true, 116 | }); 117 | }, 118 | }, 119 | }); 120 | ``` 121 | 122 | ### Icons for tags 123 | 124 | import TeenyiconsMarkdownOutline from "~icons/teenyicons/markdown-outline"; 125 | import BxBxsComponent from "~icons/bx/bxs-component"; 126 | import MaterialSymbolsLink from "~icons/material-symbols/link"; 127 | import IconoirEmptyPage from "~icons/iconoir/empty-page"; 128 | import LogosAstroIcon from "~icons/logos/astro-icon"; 129 | import LogosTypescriptIcon from "~icons/logos/typescript-icon"; 130 | 131 | Each tag can have its own color or [[icons|icon]] to help differentiate them visually. For example: 132 | 133 | - #markdown 134 | - #component 135 | - #link 136 | - #page 137 | - #Astro 138 | - #TypeScript 139 | -------------------------------------------------------------------------------- /packages/website/src/components/SidebarMulti.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Props } from "@astrojs/starlight/props"; 3 | import Default from "@astrojs/starlight/components/Sidebar.astro"; 4 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 5 | 6 | import { isContent } from "@lib/braindb.mjs"; 7 | import { getBrainDb } from "starlight-digital-garden"; 8 | import type { Document } from "@braindb/core"; 9 | 10 | const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!); 11 | 12 | const docsByChar = new Map(); 13 | const docsByDate = new Map(); 14 | (await getBrainDb().documents()).forEach((doc) => { 15 | if (isContent(doc)) { 16 | const char = firstChar(doc.title()).toUpperCase(); 17 | docsByChar.set(char, docsByChar.get(char) || []); 18 | docsByChar.get(char)?.push(doc); 19 | 20 | const date = doc.updatedAt().toISOString().split("T")[0]; 21 | docsByDate.set(date, docsByDate.get(date) || []); 22 | docsByDate.get(date)?.push(doc); 23 | } 24 | }); 25 | 26 | const comparator = new Intl.Collator("en"); 27 | const sidebarAlpha = [...docsByChar.keys()] 28 | .sort(comparator.compare) 29 | .map((char) => { 30 | return { 31 | type: "group", 32 | label: char, 33 | entries: docsByChar.get(char)?.map((doc) => { 34 | const isCurrent = doc.path() === `/${Astro.props.id}`; 35 | return { 36 | type: "link", 37 | href: doc.url(), 38 | isCurrent, 39 | // @ts-ignore 40 | label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), 41 | // @ts-ignore 42 | badge: doc.frontmatter()?.["sidebar"]?.["badge"], 43 | // @ts-ignore 44 | attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, 45 | }; 46 | }), 47 | collapsed: false, 48 | badge: undefined, 49 | }; 50 | }) as Props["sidebar"]; 51 | 52 | const sidebarDates = Array.from(docsByDate.keys()) 53 | .reverse() 54 | .map((d) => [d, d.split("-").reverse().join(".")]) 55 | .map(([date, label]) => { 56 | return { 57 | type: "group", 58 | label, 59 | entries: docsByDate.get(date)?.map((doc) => { 60 | const isCurrent = doc.path() === `/${Astro.props.id}`; 61 | return { 62 | type: "link", 63 | href: doc.url(), 64 | isCurrent, 65 | // @ts-ignore 66 | label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), 67 | // @ts-ignore 68 | badge: doc.frontmatter()?.["sidebar"]?.["badge"], 69 | // @ts-ignore 70 | attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, 71 | }; 72 | }), 73 | collapsed: false, 74 | badge: undefined, 75 | }; 76 | }) as Props["sidebar"]; 77 | --- 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 122 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/components/preview.ts: -------------------------------------------------------------------------------- 1 | import { computePosition, autoPlacement, offset } from "@floating-ui/dom"; 2 | 3 | const tooltip = document.querySelector("#linkpreview") as HTMLElement; 4 | 5 | const noPreviewClass = "no-preview"; 6 | 7 | const elements = document.querySelectorAll( 8 | ".sl-markdown-content a" 9 | ) as NodeListOf; 10 | 11 | // response may arrive after cursor left the link 12 | let currentHref: string; 13 | // it is anoying that preview shows up before user ends mouse movement 14 | // if cursor stays long enough above the link - consider it as intentional 15 | let showPreviewTimer: NodeJS.Timeout | undefined; 16 | // if cursor moves out for a short period of time and comes back we should not hide preview 17 | // if cursor moves out from link to preview window we should we should not hide preview 18 | let hidePreviewTimer: NodeJS.Timeout | undefined; 19 | 20 | function hideLinkPreview() { 21 | clearTimeout(showPreviewTimer); 22 | if (hidePreviewTimer !== undefined) return; 23 | hidePreviewTimer = setTimeout(() => { 24 | currentHref = ""; 25 | tooltip.style.display = ""; 26 | hidePreviewTimer = undefined; 27 | }, 200); 28 | } 29 | 30 | function clearTimers() { 31 | clearTimeout(showPreviewTimer); 32 | clearTimeout(hidePreviewTimer); 33 | hidePreviewTimer = undefined; 34 | } 35 | 36 | async function showLinkPreview(e: MouseEvent | FocusEvent) { 37 | const start = `${window.location.protocol}//${window.location.host}`; 38 | const target = e.target as HTMLElement; 39 | const link = target?.closest("a"); 40 | const hrefRaw = (link?.href || "") as string | SVGAnimatedString; 41 | 42 | let href = ""; 43 | let local = false; 44 | let hash = ""; 45 | let svg = false; 46 | if (typeof hrefRaw === "string") { 47 | href = hrefRaw; 48 | hash = new URL(href).hash; 49 | local = href.startsWith(start); 50 | } else { 51 | href = hrefRaw.baseVal; 52 | hash = new URL(href, window.location.origin).hash; 53 | local = href.startsWith("/"); 54 | svg = true; 55 | } 56 | 57 | const hrefWithoutAnchor = href.replace(hash, ""); 58 | const locationWithoutAnchor = window.location.href.replace( 59 | window.location.hash, 60 | "" 61 | ); 62 | 63 | currentHref = href; 64 | if (hrefWithoutAnchor === locationWithoutAnchor || !local) { 65 | hideLinkPreview(); 66 | return; 67 | } 68 | // maybe use https://developer.mozilla.org/en-US/docs/Web/API/Element/matches ? 69 | const noPreview = 70 | link?.classList.contains(noPreviewClass) || 71 | !!target.closest(`.${noPreviewClass}`); 72 | if (noPreview) { 73 | hideLinkPreview(); 74 | return; 75 | } 76 | clearTimers(); 77 | 78 | const text = await fetch(href).then((x) => x.text()); 79 | if (currentHref !== href) return; 80 | 81 | showPreviewTimer = setTimeout(() => { 82 | if (currentHref !== href) return; 83 | const doc = new DOMParser().parseFromString(text, "text/html"); 84 | const content = (doc.querySelector(".sl-markdown-content") as HTMLElement) 85 | ?.outerHTML; 86 | tooltip.innerHTML = svg 87 | ? `${doc.querySelector("h1")?.outerHTML}${content}` 88 | : content; 89 | tooltip.style.display = "block"; 90 | let offsetTop = 0; 91 | if (hash !== "") { 92 | const heading = tooltip.querySelector(hash) as HTMLElement | null; 93 | if (heading) offsetTop = heading.offsetTop; 94 | } 95 | tooltip.scroll({ top: offsetTop, behavior: "instant" }); 96 | 97 | computePosition(target, tooltip, { 98 | middleware: [offset(10), autoPlacement()], 99 | }).then(({ x, y }) => { 100 | Object.assign(tooltip.style, { 101 | left: `${x}px`, 102 | top: `${y}px`, 103 | }); 104 | }); 105 | }, 400); 106 | } 107 | 108 | // TODO: astro:page-load 109 | tooltip.addEventListener("mouseenter", clearTimers); 110 | tooltip.addEventListener("mouseleave", hideLinkPreview); 111 | 112 | const events = [ 113 | ["mouseenter", showLinkPreview], 114 | ["mouseleave", hideLinkPreview], 115 | ["focus", showLinkPreview], 116 | ["blur", hideLinkPreview], 117 | ] as const; 118 | 119 | Array.from(elements).forEach((element) => { 120 | events.forEach(([event, listener]) => { 121 | element.addEventListener(event, listener); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/euler-diagram.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Euler diagram 3 | tags: [component, diagram] 4 | description: Astro component to display a proportional Euler diagram, also known as a Venn diagram. Useful for visualizing the intersections of sets 5 | --- 6 | 7 | import EulerTagsDiagram from "@components/EulerTagsDiagram.astro"; 8 | 9 | 10 | 11 | **aka**: Venn diagram 12 | 13 | The above Euler diagram shows tags on this website. It's not a static picture; it is generated based on the content every time the website is updated. 14 | 15 | ## Installation 16 | 17 | import { Steps } from "@astrojs/starlight/components"; 18 | 19 | ### venn-isomorphic 20 | 21 | 22 | 1. Install dependencies 23 | 24 | ```bash 25 | pnpm add venn-isomorphic 26 | ``` 27 | 28 | 2. Create `EulerDiagram` component 29 | 30 | ```astro 31 | // src/components/EulerDiagram.astro 32 | --- 33 | import { createVennRenderer, type ISetOverlap } from "venn-isomorphic"; 34 | 35 | interface Props { 36 | items: ISetOverlap[]; 37 | } 38 | 39 | const items = Astro.props.items; 40 | 41 | const renderer = createVennRenderer(); 42 | const results = await renderer([items]); 43 | const [result] = results; 44 | let svg = 45 | result.status == "fulfilled" ? result.value.svg : String(result.reason); 46 | --- 47 | 48 |
49 | 50 |
51 | ``` 52 | 53 | 3. **Use component**, wherever you like 54 | 55 |
56 | 57 | ### edeap 58 | 59 | import EdeapTagsDiagram from "@components/EdeapTagsDiagram.astro"; 60 | 61 | 62 | 63 | Edeap - Euler Diagrams Drawn with Ellipses Area-Proportionally. 64 | 65 | **Pros**: 66 | 67 | - It shows areas proportional to set sizes. 68 | - It works without a headless browser. 69 | 70 | **Cons**: 71 | 72 | - The `venn-isomorphic` diagram looks better. 73 | 74 | 75 | 1. Install dependencies 76 | 77 | ```bash 78 | pnpm add edeap 79 | ``` 80 | 81 | 2. Create `EdeapDiagram` component 82 | 83 | ```astro 84 | // src/components/EdeapDiagram.astro 85 | --- 86 | import { edeapSvg, type ISetOverlap } from "edeap"; 87 | 88 | interface Props { 89 | items: ISetOverlap[]; 90 | } 91 | 92 | let svg = edeapSvg({ overlaps: items }); 93 | --- 94 | 95 |
96 | 97 |
98 | ``` 99 | 100 | 3. **Use component**, wherever you like 101 | 102 |
103 | 104 | ## Theory 105 | 106 | - [A Better Algorithm for Area Proportional Venn and Euler Diagrams](https://www.benfrederickson.com/better-venn-diagrams/) 107 | - [code](https://github.com/benfred/venn.js), [maintained fork](https://github.com/upsetjs/venn.js) 108 | - [eulerdiagrams.org](https://www.eulerdiagrams.org/) 109 | - [Euler diagrams drawn with ellipses area-proportionally (Edeap)](https://bmcbioinformatics.biomedcentral.com/articles/10.1186/s12859-021-04121-8) 110 | - [code](https://github.com/mjwybrow/edeap) 111 | - [playground](https://www.eulerdiagrams.org/edeap/) 112 | - [eulerr: Area-Proportional Euler Diagrams with Ellipses](https://lup.lub.lu.se/luur/download?func=downloadFile&recordOId=8934042&fileOId=8934043) 113 | - [code](https://github.com/jolars/eulerr2017bsc) (R) 114 | - [SPEULER: Semantics-preserving Euler Diagrams](https://www.yunhaiwang.net/Vis2021/speuler/vis21b-sub1477-cam-i7.pdf) 115 | - [EVenn: Easy to create statistically measurable Venn diagrams online](http://www.ehbio.com/test/venn/VennDoc/EVennDoc/index.html) 116 | - [nVenn: generalized, quasi-proportional Venn and Euler diagrams](https://academic.oup.com/bioinformatics/article/34/13/2322/4904268) 117 | - [UpSetR: An R Package For The Visualization Of Intersecting Sets And Their Properties](https://upset.app/) 118 | - [code](https://upset.js.org/) 119 | - [Exact and Approximate Area-proportional Circular Venn and Euler Diagrams](https://www.cs.uic.edu/~wilkinson/Publications/venneuler.pdf) 120 | - [DeepVenn – creation of area-proportional Venn diagrams using Tensorflow](https://arxiv.org/ftp/arxiv/papers/2210/2210.04597.pdf) 121 | - [A Survey of Euler Diagrams](https://kar.kent.ac.uk/35163/1/JVLC_Euler_Survey.pdf) 122 | - [Realizability of Rectangular Euler Diagrams](https://arxiv.org/pdf/2403.03801.pdf) 123 | - [Generating and Navigating Large Euler Diagrams](https://ceur-ws.org/Vol-1244/ED-paper3.pdf) 124 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/obsidian-dataview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Obsidian Dataview 3 | tags: [markdown, obsidian] 4 | description: Kind of "Obsidian Dataview" for BrainDB 5 | --- 6 | 7 | > Dataview is a live index and query engine over your personal knowledge base. You can add metadata to your notes and query them with the Dataview Query Language to list, filter, sort or group your data. Dataview keeps your queries always up to date and makes data aggregation a breeze. 8 | > 9 | > -- [Obsidian Dataview](https://blacksmithgu.github.io/obsidian-dataview/) 10 | 11 | Dataview is a plugin for [Obsidian](https://obsidian.md/). However, we can build our own "Dataview" with [[braindb]]: 12 | 13 | - Use [remark-code-hook](https://github.com/stereobooster/beoe/tree/main/packages/remark-code-hook) to catch all code blocks with the `dataview` language 14 | - Treat the code as SQL and execute wiht BrainDB 15 | - Translate the result to MDAST (or HTML) 16 | 17 | ## Installation 18 | 19 | :::caution 20 | This is experimental implementation. It can break 21 | ::: 22 | 23 | import { Steps } from "@astrojs/starlight/components"; 24 | 25 | 26 | 27 | 1. Install [[braindb#installation]] 28 | 29 | 2. Install dependencies 30 | 31 | ```bash 32 | pnpm add @braindb/remark-dataview 33 | ``` 34 | 35 | 3. Configure Astro. See note about [[rehype-plugins-for-code]]. 36 | 37 | ```js {2, 10} 38 | // astro.config.mjs 39 | import remarkDataview from "@braindb/remark-dataview"; 40 | import { getBrainDb } from "@braindb/astro"; 41 | 42 | const bdb = getBrainDb(); 43 | await bdb.ready(); 44 | 45 | export default defineConfig({ 46 | markdown: { 47 | remarkPlugins: [[remarkDataview, { bdb }]], 48 | }, 49 | }); 50 | ``` 51 | 52 | 53 | 54 | ## Examples 55 | 56 | ### Alphabetical index 57 | 58 | Query: 59 | 60 | ````md 61 | ```dataview list root_class=column-list 62 | SELECT upper(substr(frontmatter ->> '$.title', 1, 1)), dv_link() 63 | FROM documents 64 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 65 | AND frontmatter ->> '$.tags' IS NOT NULL 66 | ORDER BY frontmatter ->> '$.title' 67 | LIMIT 4; 68 | ``` 69 | ```` 70 | 71 | Output: 72 | 73 | ```dataview list root_class=column-list 74 | SELECT upper(substr(frontmatter ->> '$.title', 1, 1)), dv_link() 75 | FROM documents 76 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 77 | AND frontmatter ->> '$.tags' IS NOT NULL 78 | ORDER BY frontmatter ->> '$.title' 79 | LIMIT 4; 80 | ``` 81 | 82 | ### Recently changed 83 | 84 | Query: 85 | 86 | ````md 87 | ```dataview list root_class=column-list 88 | SELECT date(updated_at / 1000, 'unixepoch'), dv_link() 89 | FROM documents 90 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 91 | AND frontmatter ->> '$.tags' IS NOT NULL 92 | ORDER BY updated_at DESC 93 | LIMIT 3; 94 | ``` 95 | ```` 96 | 97 | Output: 98 | 99 | ```dataview list root_class=column-list 100 | SELECT date(updated_at / 1000, 'unixepoch'), dv_link() 101 | FROM documents 102 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 103 | AND frontmatter ->> '$.tags' IS NOT NULL 104 | ORDER BY updated_at DESC 105 | LIMIT 3; 106 | ``` 107 | 108 | ### Task list 109 | 110 | Query: 111 | 112 | ````md 113 | ```dataview list 114 | SELECT dv_link(), dv_task() 115 | FROM tasks JOIN documents ON documents.path = tasks.from 116 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 117 | AND frontmatter ->> '$.tags' IS NOT NULL 118 | ORDER BY updated_at DESC, path, tasks.start 119 | LIMIT 2; 120 | ``` 121 | ```` 122 | 123 | Output: 124 | 125 | ```dataview list 126 | SELECT dv_link(), dv_task() 127 | FROM tasks JOIN documents ON documents.path = tasks.from 128 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 129 | AND frontmatter ->> '$.tags' IS NOT NULL 130 | ORDER BY updated_at DESC, path, tasks.start 131 | LIMIT 2; 132 | ``` 133 | 134 | ### Tags list 135 | 136 | Query: 137 | 138 | ````md 139 | ```dataview list root_class=column-list 140 | SELECT '#' || tags.value as tag, dv_link() 141 | FROM documents, json_each(frontmatter, '$.tags') tags 142 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 143 | AND frontmatter ->> '$.tags' IS NOT NULL 144 | ORDER BY tag 145 | LIMIT 3; 146 | ``` 147 | ```` 148 | 149 | Output: 150 | 151 | ```dataview list root_class=column-list 152 | SELECT '#' || tags.value as tag, dv_link() 153 | FROM documents, json_each(frontmatter, '$.tags') tags 154 | WHERE (frontmatter ->> '$.draft' IS NULL OR frontmatter ->> '$.draft' = false) 155 | AND frontmatter ->> '$.tags' IS NOT NULL 156 | ORDER BY tag 157 | LIMIT 3; 158 | ``` 159 | -------------------------------------------------------------------------------- /packages/starlight-digital-garden/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | StarlightPlugin, 3 | StarlightUserConfig, 4 | } from "@astrojs/starlight/types"; 5 | import type { AstroIntegrationLogger } from "astro"; 6 | import { defineIntegration } from "astro-integration-kit"; 7 | import { z } from "astro/zod"; 8 | import rehypeExternalLinks from "rehype-external-links"; 9 | import robotsTxt from "astro-robots-txt"; 10 | import remarkDataview from "@braindb/remark-dataview"; 11 | 12 | import { brainDbAstro, getBrainDb } from "@braindb/astro"; 13 | export { getBrainDb }; 14 | 15 | export const astroDigitalGarden = defineIntegration({ 16 | name: "astro-digital-garden", 17 | optionsSchema: z 18 | .object({ 19 | remarkDataview: z.boolean(), 20 | rehypeExternalLinks: z.union([ 21 | z.literal("arrow"), 22 | z.literal("icon"), 23 | z.literal("off"), 24 | ]), 25 | }) 26 | .partial() 27 | .optional(), 28 | setup({ options }) { 29 | return { 30 | hooks: { 31 | "astro:config:setup": ({ config, updateConfig }) => { 32 | const newConfig = { 33 | markdown: { 34 | remarkPlugins: [ 35 | ...(options?.remarkDataview === true 36 | ? [[remarkDataview, { getBrainDb }]] 37 | : []), 38 | ...config.markdown.remarkPlugins, 39 | ], 40 | rehypePlugins: [ 41 | ...(options?.rehypeExternalLinks === "off" 42 | ? [] 43 | : [ 44 | [ 45 | rehypeExternalLinks, 46 | { 47 | target: "_blank", 48 | rel: ["nofollow", "noopener"], 49 | ...(options?.rehypeExternalLinks === "icon" 50 | ? { 51 | content: { type: "text", value: "" }, 52 | contentProperties: (x: any) => { 53 | const hostname = new URL(x.properties.href) 54 | .hostname; 55 | return { 56 | class: "external-icon", 57 | style: `--icon: url(https://external-content.duckduckgo.com/ip3/${hostname}.ico)`, 58 | }; 59 | }, 60 | } 61 | : { 62 | content: { type: "text", value: " ↗" }, // ⤴ 63 | contentProperties: { 64 | "aria-hidden": true, 65 | class: "no-select", 66 | }, 67 | }), 68 | }, 69 | ], 70 | ]), 71 | ...config.markdown.rehypePlugins, 72 | ], 73 | }, 74 | }; 75 | updateConfig(newConfig); 76 | }, 77 | }, 78 | }; 79 | }, 80 | }); 81 | 82 | type SDGOptions = { 83 | robotsTxt?: boolean; 84 | rehypeExternalLinks?: "arrow" | "icon"; 85 | remarkDataview?: boolean; 86 | // TODO: BrainDbOptions 87 | // TODO: maybe options to disable previews, backlinks, wikilinks etc? 88 | }; 89 | 90 | export default function starlightDigitalGarden( 91 | options?: SDGOptions 92 | ): StarlightPlugin { 93 | return { 94 | name: "starlight-digital-garden", 95 | hooks: { 96 | setup({ 97 | config: starlightConfig, 98 | logger, 99 | updateConfig: updateStarlightConfig, 100 | addIntegration, 101 | }) { 102 | addIntegration(astroDigitalGarden(options)); 103 | addIntegration(brainDbAstro()); 104 | if (options?.robotsTxt !== false) addIntegration(robotsTxt()); 105 | 106 | updateStarlightConfig({ 107 | // pagination doesn't make sense in the context of digital garden 108 | pagination: false, 109 | components: { 110 | ...starlightConfig.components, 111 | ...overrideStarlightComponent( 112 | starlightConfig.components, 113 | logger, 114 | "TableOfContents" 115 | ), 116 | ...overrideStarlightComponent( 117 | starlightConfig.components, 118 | logger, 119 | "PageFrame" 120 | ), 121 | }, 122 | }); 123 | }, 124 | }, 125 | }; 126 | } 127 | 128 | function overrideStarlightComponent( 129 | components: StarlightUserConfig["components"], 130 | logger: AstroIntegrationLogger, 131 | component: keyof NonNullable 132 | ) { 133 | if (components?.[component]) { 134 | logger.warn( 135 | `It looks like you already have a \`${component}\` component override in your Starlight configuration.` 136 | ); 137 | logger.warn( 138 | `To use \`starlight-digital-garden\`, either remove your override or update it to render the content from \`starlight-digital-garden/overrides/${component}.astro\`.` 139 | ); 140 | 141 | return {}; 142 | } 143 | 144 | return { 145 | [component]: `starlight-digital-garden/overrides/${component}.astro`, 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /packages/website/src/components/graphRenderer.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** 3 | * Graphology SVG Renderer 4 | * 5 | * Copy-paste from original package to fix some things in place 6 | */ 7 | import helpers from "graphology-svg/helpers"; 8 | import defaults from "graphology-svg/defaults"; 9 | // import line from "graphology-svg/components/edges/line"; 10 | // import circle from "graphology-svg/components/nodes/circle"; 11 | // import nodeLabelDefault from "graphology-svg/components/nodeLabels/default"; 12 | 13 | export const { DEFAULTS } = defaults; 14 | 15 | function nodeReducer(settings: any, node: any, attr: any) { 16 | return { 17 | ...defaults.DEFAULT_NODE_REDUCER(settings, node, attr), 18 | url: attr.url, 19 | id: node, 20 | }; 21 | } 22 | 23 | function drawCircle(_settings, data) { 24 | const circle = ``; 25 | if (!data.url) return circle; 26 | return `${circle}`; 29 | } 30 | 31 | function drawLabel(settings, data) { 32 | const label = `${helpers.escape(data.label)}`; 37 | if (!data.url) return ""; 38 | return ``; 41 | } 42 | 43 | const marker = ` 44 | 45 | 46 | 47 | `; 48 | 49 | function drawEdge(_settings, data, sourceData, targetData) { 50 | // return ``; 56 | 57 | const radius = 14; 58 | const length = Math.sqrt( 59 | Math.pow(sourceData.y - targetData.y, 2) + 60 | Math.pow(sourceData.x - targetData.x, 2) 61 | ); 62 | const coefficientX = (sourceData.x - targetData.x) / length; 63 | const coefficientY = (sourceData.y - targetData.y) / length; 64 | const arrowX = targetData.x + coefficientX * radius; 65 | const arrowY = targetData.y + coefficientY * radius; 66 | 67 | return ``; 70 | } 71 | 72 | const components = { 73 | nodes: { 74 | circle: drawCircle, 75 | }, 76 | edges: { 77 | line: drawEdge, 78 | }, 79 | nodeLabels: { 80 | default: drawLabel, 81 | }, 82 | }; 83 | 84 | export function renderer(graph: any, settings: any) { 85 | // Reducing nodes 86 | const nodeData = reduceNodes(graph, settings); 87 | 88 | // Drawing edges 89 | const edgesStrings = []; 90 | graph.forEachEdge(function (edge, attr, source, target) { 91 | // Reducing edge 92 | if (typeof settings.edges.reducer === "function") 93 | attr = settings.edges.reducer(settings, edge, attr); 94 | 95 | attr = defaults.DEFAULT_EDGE_REDUCER(settings, edge, attr); 96 | 97 | edgesStrings.push( 98 | components.edges[attr.type]( 99 | settings, 100 | attr, 101 | nodeData[source], 102 | nodeData[target] 103 | ) 104 | ); 105 | }); 106 | 107 | // Drawing nodes and labels 108 | // TODO: should we draw in size order to avoid weird overlaps? Should we run noverlap? 109 | const nodesStrings = []; 110 | const nodeLabelsStrings = []; 111 | let k; 112 | for (k in nodeData) { 113 | nodesStrings.push( 114 | components.nodes[nodeData[k].type](settings, nodeData[k]) 115 | ); 116 | nodeLabelsStrings.push( 117 | components.nodeLabels[nodeData[k].labelType](settings, nodeData[k]) 118 | ); 119 | } 120 | 121 | return ` 124 | ${marker} 125 | ${edgesStrings.join("")} 126 | ${nodesStrings.join("")} 127 | ${nodeLabelsStrings.join("")} 128 | `; 129 | } 130 | 131 | function reduceNodes(graph, settings) { 132 | const width = settings.width, 133 | height = settings.height; 134 | 135 | let xBarycenter = 0, 136 | yBarycenter = 0, 137 | totalWeight = 0; 138 | 139 | const data = {}; 140 | 141 | graph.forEachNode(function (node, attr) { 142 | // Applying user's reducing logic 143 | if (typeof settings.nodes.reducer === "function") 144 | attr = settings.nodes.reducer(settings, node, attr); 145 | 146 | attr = nodeReducer(settings, node, attr); 147 | data[node] = attr; 148 | 149 | // Computing rescaling items 150 | xBarycenter += attr.size * attr.x; 151 | yBarycenter += attr.size * attr.y; 152 | totalWeight += attr.size; 153 | }); 154 | 155 | xBarycenter /= totalWeight; 156 | yBarycenter /= totalWeight; 157 | 158 | let d, ratio, n; 159 | let dMax = -Infinity; 160 | 161 | let k; 162 | 163 | for (k in data) { 164 | n = data[k]; 165 | d = Math.pow(n.x - xBarycenter, 2) + Math.pow(n.y - yBarycenter, 2); 166 | 167 | if (d > dMax) dMax = d; 168 | } 169 | 170 | ratio = 171 | (Math.min(width, height) - 2 * settings.margin) / (2 * Math.sqrt(dMax)); 172 | 173 | for (k in data) { 174 | n = data[k]; 175 | 176 | n.x = width / 2 + (n.x - xBarycenter) * ratio; 177 | n.y = height / 2 + (n.y - yBarycenter) * ratio; 178 | 179 | n.size *= ratio; // TODO: keep? 180 | } 181 | 182 | return data; 183 | } 184 | -------------------------------------------------------------------------------- /packages/website/src/components/toc.js: -------------------------------------------------------------------------------- 1 | // Create some WeakMaps to store the distances to the top and 2 | // bottom of each link 3 | const linkStarts = new WeakMap(); 4 | const linkEnds = new WeakMap(); 5 | const sectionsMap = new Map(); 6 | 7 | // TODO: astro:page-load 8 | addIntersectionObserver(); 9 | addResizeObserver(); 10 | 11 | const tags = ["h2", "h3", "h4", "h5"]; 12 | const tagClasses = tags.map((x) => `sl-heading-wrapper level-${x}`); 13 | function findPreviousSibling(el) { 14 | let result = []; 15 | do { 16 | if (tags.includes(el.tagName.toLowerCase())) result.push(el); 17 | if (tagClasses.includes(el.className)) result.push(el.firstChild); 18 | tagClasses.includes(el.className); 19 | if (el.previousElementSibling == null) break; 20 | el = el.previousElementSibling; 21 | if ( 22 | result.length > 0 && 23 | !( 24 | tags.includes(el.tagName.toLowerCase()) || 25 | tagClasses.includes(el.className) 26 | ) 27 | ) 28 | break; 29 | } while (el); 30 | return result; 31 | } 32 | 33 | function addIntersectionObserver() { 34 | const observer = new IntersectionObserver((sections) => { 35 | sections.forEach((section) => { 36 | const ids = 37 | sectionsMap.get(section.target)?.ids || 38 | findPreviousSibling(section.target).map((h) => h.getAttribute("id")); 39 | sectionsMap.set(section.target, { 40 | ids, 41 | intersectionRatio: section.intersectionRatio, 42 | }); 43 | }); 44 | 45 | const idMap = {}; 46 | [...sectionsMap.values()].forEach(({ ids, intersectionRatio }) => { 47 | ids.forEach((id) => { 48 | idMap[id] = (idMap[id] || 0) + intersectionRatio; 49 | }); 50 | }); 51 | 52 | Object.entries(idMap).forEach(([id, intersectionRatio]) => { 53 | // Get the link to this section's heading 54 | const link = document.querySelector(`nav.toc-new li a[href="#${id}"]`); 55 | if (!link) return; 56 | 57 | // Add/remove the .active class based on whether the 58 | // section is visible 59 | const addRemove = intersectionRatio > 0 ? "add" : "remove"; 60 | link.classList[addRemove]("active"); 61 | }); 62 | 63 | updatePath(); 64 | }); 65 | 66 | // Observe all the sections of the article 67 | document 68 | .querySelectorAll(".sl-markdown-content > :not(h2, h3, h4, h5)") 69 | .forEach((section) => { 70 | observer.observe(section); 71 | }); 72 | } 73 | 74 | function addResizeObserver() { 75 | if (!document.querySelector("nav.toc-new")) return; 76 | const observer = new ResizeObserver(() => { 77 | drawPath(); 78 | updatePath(); 79 | }); 80 | observer.observe(document.querySelector("nav.toc-new")); 81 | } 82 | 83 | function drawPath() { 84 | const path = document.querySelector("path.toc-marker"); 85 | const links = Array.from(document.querySelectorAll("nav.toc-new a")); 86 | if (!links.length || !path) return; 87 | 88 | // Start with an empty array of path data values (joined with 89 | // spaces later) 90 | let pathData = []; 91 | let left = 0; // The last x position / indentation 92 | 93 | // Iterate over each link to build up the pathData 94 | links.forEach((link, i) => { 95 | const x = link.offsetLeft; 96 | const y = link.offsetTop; 97 | const height = link.offsetHeight; 98 | if (i === 0) { 99 | // The top of the first link starts at 0px along the path. 100 | linkStarts.set(link, 0); 101 | // Like drawing with a pen... 102 | // 'M'ove to the top left of the first link, 103 | // and then draw a 'L'ine to the bottom left 104 | pathData.push("M", x, y, "L", x, y + height); 105 | } else { 106 | // If the current link is indented differently than the last, 107 | // then come down to the current link's top before moving 108 | // left or right. This ensures we get 90-degrees angle at the 109 | // corners. 110 | if (left !== x) pathData.push("L", left, y); 111 | 112 | // Draw a line to the top left of the current link 113 | pathData.push("L", x, y); 114 | 115 | // Apply the current path data to the path element 116 | path.setAttribute("d", pathData.join(" ")); 117 | 118 | // Get the total length of the path now that it extends 119 | // to the top of this link, and store it in our linkStarts 120 | // WeakMap. 121 | linkStarts.set(link, path.getTotalLength()); 122 | 123 | // Draw a line to the bottom left of the current link 124 | pathData.push("L", x, y + height); 125 | } 126 | 127 | // Save the current link's x position to compare with the next 128 | // link 129 | left = x; 130 | 131 | // Apply the current path data to the path element again 132 | path.setAttribute("d", pathData.join(" ")); 133 | 134 | // Get the length of the path that now extends to the 135 | // bottom of this link, and store it in our linkEnds WeakMap. 136 | linkEnds.set(link, path.getTotalLength()); 137 | }); 138 | } 139 | 140 | function updatePath() { 141 | const path = document.querySelector("path.toc-marker"); 142 | const pathLength = path?.getTotalLength(); 143 | const activeLinks = document.querySelectorAll("nav.toc-new a.active"); 144 | if (!activeLinks || !path) return; 145 | 146 | let linkStart = pathLength; 147 | let linkEnd = 0; 148 | activeLinks.forEach((link) => { 149 | // Set linkStart to the top of the earliest active link 150 | linkStart = Math.min(linkStart, linkStarts.get(link)); 151 | // Set linkEnd to the bottom of the furthest active link 152 | linkEnd = Math.max(linkEnd, linkEnds.get(link)); 153 | }); 154 | // If there are no active links, hide the path 155 | path.style.display = activeLinks.length ? "inline" : "none"; 156 | // FINALLY, do the thing! 157 | path.setAttribute( 158 | "stroke-dasharray", 159 | `1 ${linkStart} ${linkEnd - linkStart} ${pathLength}` 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/mermaid-diagrams-in-markdown.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mermaid diagrams in Markdown 3 | tags: [markdown, code-fences, diagram] 4 | description: Embed Mermaid diagrams inside code fences in Markdown. No client-side JavaScript. Dark mode support. SVG 5 | --- 6 | 7 | ```mermaid alt="example of the Mermaid diagram" 8 | --- 9 | config: 10 | sankey: 11 | showValues: false 12 | --- 13 | sankey-beta 14 | 15 | Agricultural 'waste',Bio-conversion,124.729 16 | Bio-conversion,Liquid,0.597 17 | Bio-conversion,Losses,26.862 18 | Bio-conversion,Solid,280.322 19 | Bio-conversion,Gas,81.144 20 | Biofuel imports,Liquid,35 21 | Biomass imports,Solid,35 22 | Coal imports,Coal,11.606 23 | Coal reserves,Coal,63.965 24 | Coal,Solid,75.571 25 | District heating,Industry,10.639 26 | District heating,Heating and cooling - commercial,22.505 27 | District heating,Heating and cooling - homes,46.184 28 | Electricity grid,Over generation / exports,104.453 29 | Electricity grid,Heating and cooling - homes,113.726 30 | Electricity grid,H2 conversion,27.14 31 | Electricity grid,Industry,342.165 32 | Electricity grid,Road transport,37.797 33 | Electricity grid,Agriculture,4.412 34 | Electricity grid,Heating and cooling - commercial,40.858 35 | Electricity grid,Losses,56.691 36 | Electricity grid,Rail transport,7.863 37 | Electricity grid,Lighting & appliances - commercial,90.008 38 | Electricity grid,Lighting & appliances - homes,93.494 39 | Gas imports,Ngas,40.719 40 | Gas reserves,Ngas,82.233 41 | Gas,Heating and cooling - commercial,0.129 42 | Gas,Losses,1.401 43 | Gas,Thermal generation,151.891 44 | Gas,Agriculture,2.096 45 | Gas,Industry,48.58 46 | Geothermal,Electricity grid,7.013 47 | H2 conversion,H2,20.897 48 | H2 conversion,Losses,6.242 49 | H2,Road transport,20.897 50 | Hydro,Electricity grid,6.995 51 | Liquid,Industry,121.066 52 | Liquid,International shipping,128.69 53 | Liquid,Road transport,135.835 54 | Liquid,Domestic aviation,14.458 55 | Liquid,International aviation,206.267 56 | Liquid,Agriculture,3.64 57 | Liquid,National navigation,33.218 58 | Liquid,Rail transport,4.413 59 | Marine algae,Bio-conversion,4.375 60 | Ngas,Gas,122.952 61 | Nuclear,Thermal generation,839.978 62 | Oil imports,Oil,504.287 63 | Oil reserves,Oil,107.703 64 | Oil,Liquid,611.99 65 | Other waste,Solid,56.587 66 | Other waste,Bio-conversion,77.81 67 | Pumped heat,Heating and cooling - homes,193.026 68 | Pumped heat,Heating and cooling - commercial,70.672 69 | Solar PV,Electricity grid,59.901 70 | Solar Thermal,Heating and cooling - homes,19.263 71 | Solar,Solar Thermal,19.263 72 | Solar,Solar PV,59.901 73 | Solid,Agriculture,0.882 74 | Solid,Thermal generation,400.12 75 | Solid,Industry,46.477 76 | Thermal generation,Electricity grid,525.531 77 | Thermal generation,Losses,787.129 78 | Thermal generation,District heating,79.329 79 | Tidal,Electricity grid,9.452 80 | UK land based bioenergy,Bio-conversion,182.01 81 | Wave,Electricity grid,19.013 82 | Wind,Electricity grid,289.366 83 | ``` 84 | 85 | Example taken from [mermaid.js.org](https://mermaid.js.org/syntax/sankey.html). 86 | 87 | There are many diagramming languages (see [text-to-diagram](https://stereobooster.com/posts/text-to-diagram/)). [Mermaid](https://mermaid.js.org/) seems to be popular, as it is supported by GitHub. 88 | 89 | There are **two options**: 90 | 91 | - `rehype-mermaid` - the original plugin. Battle-tested and well supported. 92 | - [`@beoe/rehype-mermaid`](https://beoe.stereobooster.com/diagrams/mermaid/) - my experiment. It is based on `mermaid-isomorphic` (the same one used by `rehype-mermaid`). It also adds: 93 | - [Selector-based dark mode](https://github.com/remcohaszing/rehype-mermaid/issues/16). Used by Starlight and others. 94 | - Cache to prevent the permanent launch of a headless browser. 95 | - Out-of-the-box compatibility with [[svg-pan-zoom|pan and zoom "plugin"]]. 96 | 97 | ## @beoe/rehype-mermaid 98 | 99 | ### Installation 100 | 101 | import { Steps } from "@astrojs/starlight/components"; 102 | 103 | 104 | 1. Install dependencies 105 | 106 | ```bash 107 | pnpm add @beoe/rehype-mermaid playwright 108 | ``` 109 | 110 | 2. Update `package.json` 111 | 112 | ```json {3} 113 | // package.json 114 | "scripts": { 115 | "postinstall": "playwright install chromium" 116 | } 117 | ``` 118 | 119 | 3. Configure Astro. See note about [[rehype-plugins-for-code]]. 120 | 121 | ```js {2, 7-15} 122 | // astro.config.mjs 123 | import { rehypeMermaid } from "@beoe/rehype-mermaid"; 124 | 125 | export default defineConfig({ 126 | markdown: { 127 | rehypePlugins: [ 128 | [ 129 | rehypeMermaid, 130 | { 131 | strategy: "file", // alternatively use "data-url" 132 | fsPath: "public/beoe", // add this to gitignore 133 | webPath: "/beoe", 134 | darkScheme: "class", 135 | }, 136 | ], 137 | ], 138 | }, 139 | }); 140 | ``` 141 | 142 | 4. Add line to `.gitignore`: 143 | 144 | ```gitignore 145 | public/beoe 146 | ``` 147 | 148 | 5. Add CSS for **dark mode**: 149 | 150 | ```css 151 | // src/styles/custom.css 152 | html[data-theme="light"] .beoe-dark { 153 | display: none; 154 | } 155 | 156 | html[data-theme="dark"] .beoe-light { 157 | display: none; 158 | } 159 | ``` 160 | 161 | 6. **Optional** install dependency for **cache** 162 | 163 | ```bash 164 | pnpm add @beoe/cache 165 | ``` 166 | 167 | 7. **Optional** configure cache 168 | 169 | ```js {2,4,16} 170 | // astro.config.mjs 171 | import { getCache } from "@beoe/cache"; 172 | 173 | const cache = await getCache(); 174 | 175 | export default defineConfig({ 176 | markdown: { 177 | rehypePlugins: [ 178 | [ 179 | rehypeMermaid, 180 | { 181 | strategy: "file", 182 | fsPath: "public/beoe", 183 | webPath: "/beoe", 184 | darkScheme: "class", 185 | cache, 186 | }, 187 | ], 188 | ], 189 | }, 190 | }); 191 | ``` 192 | 193 | 8. **Optional** add [[svg-pan-zoom#installation|pan and zoom for diagrams]] 194 | 195 | 196 | 197 | ## Example 198 | 199 | ````md 200 | // example.md 201 | 202 | ```mermaid alt="example of the Mermaid diagram" 203 | flowchart LR 204 | Start --> Stop 205 | ``` 206 | ```` 207 | 208 | ```mermaid alt="example of the Mermaid diagram" 209 | flowchart LR 210 | Start --> Stop 211 | ``` 212 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/gnuplot-diagram.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Gnuplot diagram 3 | tags: [markdown, code-fences, diagram] 4 | description: Gnuplot inside "code fences" converted to diagrams - static SVGs, no client-side JavaScript 5 | --- 6 | 7 | ```gnuplot alt="example of the Gnuplot diagram" 8 | # Plotting filledcurves with different transparencies 9 | # 10 | # AUTHOR: Hagen Wierstorf 11 | 12 | set samples 1000, 500 13 | 14 | set border linewidth 1.5 15 | 16 | # Axes 17 | 18 | set style line 11 lc rgb '#808080' lt 1 19 | set border 3 back ls 11 20 | set tics nomirror out scale 0.75 21 | 22 | # Grid 23 | 24 | set style line 12 lc rgb'#808080' lt 0 lw 1 25 | set grid back ls 12 26 | 27 | set style fill noborder 28 | set style function filledcurves y1=0 29 | set clip two 30 | 31 | Gauss(x,mu,sigma) = 1./(sigma*sqrt(2*pi)) * exp( -(x-mu)**2 / (2*sigma**2) ) 32 | d1(x) = Gauss(x, 0.5, 0.5) 33 | d2(x) = Gauss(x, 2., 1.) 34 | d3(x) = Gauss(x, -1., 2.) 35 | 36 | set xrange [-5:5] 37 | set yrange [0:1] 38 | 39 | unset colorbox 40 | 41 | set key title "Gaussian Distribution" 42 | set key top left Left reverse samplen 1 43 | 44 | set lmargin 6 45 | plot d1(x) fs transparent solid 0.75 lc rgb "forest-green" title 'µ= 0.5 σ=0.5', \ 46 | d2(x) fs transparent solid 0.50 lc rgb "gold" title 'µ= 2.0 σ=1.0', \ 47 | d3(x) fs transparent solid 0.25 lc rgb "red" title 'µ=-1.0 σ=2.0' 48 | ``` 49 | 50 | Example taken [here](http://gnuplotting.org/filledcurves-with-different-transparency/index.html) 51 | 52 | ## About 53 | 54 | [Gnuplot](https://gnuplot.sourceforge.net/) is a graphing utility. The source code is copyrighted but freely distributed (i.e., you don't have to pay for it). It was originally created to allow scientists and students to visualize mathematical functions and data interactively. It is also used as a plotting engine by third-party applications like Octave. Gnuplot has been supported and under active development since **1986**. 55 | 56 | ## Installation 57 | 58 | import { Steps } from "@astrojs/starlight/components"; 59 | 60 | 61 | 1. Install dependencies 62 | 63 | ```bash 64 | pnpm add @beoe/rehype-gnuplot 65 | ``` 66 | 67 | 2. Configure Astro. See note about [[rehype-plugins-for-code]]. 68 | 69 | ```js {2, 7-14} 70 | // astro.config.mjs 71 | import { rehypeGnuplot } from "@beoe/rehype-gnuplot"; 72 | 73 | export default defineConfig({ 74 | markdown: { 75 | rehypePlugins: [ 76 | [ 77 | rehypeGnuplot, 78 | { 79 | strategy: "file", // alternatively use "data-url" 80 | fsPath: "public/beoe", // add this to gitignore 81 | webPath: "/beoe", 82 | }, 83 | ], 84 | ], 85 | }, 86 | }); 87 | ``` 88 | 89 | 4. Add line to `.gitignore`: 90 | 91 | ```gitignore 92 | public/beoe 93 | ``` 94 | 95 | 3. **Optional** install dependency for cache 96 | 97 | ```bash 98 | pnpm add @beoe/cache 99 | ``` 100 | 101 | 4. **Optional** configure cache 102 | 103 | ```js {2, 4, 15} 104 | // astro.config.mjs 105 | import { getCache } from "@beoe/cache"; 106 | 107 | const cache = await getCache(); 108 | 109 | export default defineConfig({ 110 | markdown: { 111 | rehypePlugins: [ 112 | [ 113 | rehypeGnuplot, 114 | { 115 | strategy: "file", 116 | fsPath: "public/beoe", 117 | webPath: "/beoe", 118 | cache, 119 | }, 120 | ], 121 | ], 122 | }, 123 | }); 124 | ``` 125 | 126 | 5. **Optional** add [[svg-pan-zoom#installation|pan and zoom for diagrams]] 127 | 128 | 129 | 130 | ## Example 131 | 132 | ````md 133 | ```gnuplot alt="Plot of sin(x)" 134 | plot [-10:10] sin(x) 135 | ``` 136 | ```` 137 | 138 | ```gnuplot alt="Plot of sin(x)" 139 | plot [-10:10] sin(x) 140 | ``` 141 | 142 | ## Bonus: XKCD-style 143 | 144 | ```gnuplot alt="example of the Gnuplot diagram in XKCD style" 145 | # Gnuplot file that plots a couple of functions in the xkcd style 146 | # Not a proper rendition, as the axis are still straight lines 147 | # Also, when plotting functions, the jiggling can only be done in 148 | # the y coordinate. For datapoints, one can do the jiggling on both 149 | # x and y. 150 | # The proper way to do this would be to write a xkcd terminal for 151 | # gnuplot, but this is way beyond the time that I can use on this... 152 | 153 | # You can download the HumorSans font from here: http://antiyawn.com/uploads/Humor-Sans.ttf 154 | # To use it in the eps terminal, you have to convert it to a postscript pfa format 155 | # Since I already did that, you can just use the file included in this git repository. 156 | 157 | set terminal svg enhanced font "HumorSans,Patrick Hand,Chalkboard,Comic Sans MS,18" 158 | 159 | # Some line types 160 | set style line 10 lt 1 lc rgbcolor "#ffffff" lw 8 #thick white 161 | set style line 11 lt 1 lc rgbcolor "#000000" lw 2 #black 162 | 163 | set style line 1 lt 1 lc rgbcolor "#ff0000" lw 2 #red 164 | set style line 2 lt 1 lc rgbcolor "#0000ff" lw 2 #blue 165 | 166 | # No border with tics 167 | set border 0 168 | 169 | set noxtics 170 | set noytics 171 | 172 | # Show the axis 173 | set xzeroaxis ls 11 174 | set yzeroaxis ls 11 175 | 176 | #Arrow end to the axis 177 | set arrow from graph 0.95, first 0 to graph 1, first 0 size 2,3 front 178 | set arrow from first 0, graph 0.95 to first 0, graph 1 size 2,3 front 179 | 180 | set yrange [-1.1:1.1] 181 | set xrange [-1:15] 182 | 183 | set key bottom 184 | 185 | set label 'Wasted time' at 11,0.7 right 186 | set arrow from 11.1,0.7 to 12,((12/15.0)**2) ls 11 187 | 188 | set title 'Check this out! XKCD in Gnuplot' 189 | 190 | #Jiggling functions 191 | range = 2.0 #Range for the absolute jiggle 192 | jigglea(x) = x+range*(2*(rand(0)-0.5)*0.005) #Absolute (as a fraction of the range) 193 | jiggle(x) = x*(1+(2*rand(0)-0.5)*0.015) #Relative +-1.5% (as a fraction of the y value) 194 | 195 | dpsin(x) = sin(x) * exp(-0.1 * (x - 5) ** 2) 196 | dpcos(x) = - cos(x) * exp(-0.1 * (x - 5) ** 2) 197 | 198 | plot jiggle(dpsin(x)) ls 10 t '', \ 199 | jiggle(dpsin(x)) ls 1 t 'Damped Sin',\ 200 | jiggle(dpcos(x)) ls 10 t '', \ 201 | jiggle(dpcos(x)) ls 2 t 'Damped Cos',\ 202 | jigglea((x/15)**2) ls 10 t '',\ 203 | jigglea((x/15)**2) ls 11 t '' 204 | ``` 205 | 206 | Example taken [here](https://rfonseca.github.io/xkcd-gnuplot/). 207 | 208 | **Note**: In order for the example to work as SVG, I changed the following lines: 209 | 210 | ```diff 211 | - set term postscript eps font 'HumorSans' fontfile 'Humor-Sans.pfa' 22 212 | - set output 'xkcd.eps' 213 | + set terminal svg enhanced font "HumorSans,Patrick Hand,Chalkboard,Comic Sans MS,18" 214 | ``` 215 | 216 | You may use any font you like and load if, for example: 217 | 218 | - [Humor Sans](https://github.com/shreyankg/xkcd-desktop/blob/master/Humor-Sans.ttf) 219 | - [Patrick Hand](https://fonts.google.com/specimen/Patrick+Hand) 220 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/link-previews.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Link previews 3 | tags: [component] 4 | description: Lets you preview a page when you hover the cursor over an internal link, without needing to navigate to that page 5 | --- 6 | 7 | **aka** popover previews 8 | 9 | ## Options 10 | 11 | First of all, we need a popover library, for example: 12 | 13 | - [Floating UI](https://floating-ui.com/) 14 | - [Popper](https://popper.js.org/) 15 | - [Tippy.js](https://atomiks.github.io/tippyjs/) 16 | 17 | Then, we need to decide how to fetch content for the page. For example: 18 | 19 | - Fetch HTML and extract the header (`h1`) and content (`.sl-markdown-content`) with `DOMParser`. 20 | - Fetch HTML and extract metadata, such as OpenGraph tags, like `title`, `description`, and `image`. 21 | - Fetch a special JSON file (which needs to be generated upfront). 22 | 23 | ## Implementation 24 | 25 | import { Steps } from "@astrojs/starlight/components"; 26 | 27 | 28 | 29 | 1. Install dependencies 30 | ```bash 31 | pnpm add @floating-ui/dom 32 | ``` 33 | 2. Add `preview.ts` 34 | 35 | ```ts 36 | //src/components/preview.ts 37 | import { computePosition, autoPlacement, offset } from "@floating-ui/dom"; 38 | 39 | const tooltip = document.querySelector("#linkpreview") as HTMLElement; 40 | 41 | const elements = document.querySelectorAll( 42 | ".sl-markdown-content a" 43 | ) as NodeListOf; 44 | 45 | // response may arrive after cursor left the link 46 | let currentHref: string; 47 | // it is anoying that preview shows up before user ends mouse movement 48 | // if cursor stays long enough above the link - consider it as intentional 49 | let showPreviewTimer: NodeJS.Timeout | undefined; 50 | // if cursor moves out for a short period of time and comes back we should not hide preview 51 | // if cursor moves out from link to preview window we should we should not hide preview 52 | let hidePreviewTimer: NodeJS.Timeout | undefined; 53 | 54 | function hideLinkPreview() { 55 | clearTimeout(showPreviewTimer); 56 | if (hidePreviewTimer !== undefined) return; 57 | hidePreviewTimer = setTimeout(() => { 58 | currentHref = ""; 59 | tooltip.style.display = ""; 60 | hidePreviewTimer = undefined; 61 | }, 200); 62 | } 63 | 64 | function clearTimers() { 65 | clearTimeout(showPreviewTimer); 66 | clearTimeout(hidePreviewTimer); 67 | hidePreviewTimer = undefined; 68 | } 69 | 70 | tooltip.addEventListener("mouseenter", clearTimers); 71 | tooltip.addEventListener("mouseleave", hideLinkPreview); 72 | 73 | async function showLinkPreview(e: MouseEvent | FocusEvent) { 74 | const start = `${window.location.protocol}//${window.location.host}`; 75 | const target = e.target as HTMLElement; 76 | const href = target?.closest("a")?.href || ""; 77 | const hash = new URL(href).hash; 78 | 79 | const hrefWithoutAnchor = href.replace(hash, ""); 80 | const locationWithoutAnchor = window.location.href.replace( 81 | window.location.hash, 82 | "" 83 | ); 84 | 85 | currentHref = href; 86 | if ( 87 | hrefWithoutAnchor === locationWithoutAnchor || 88 | !href.startsWith(start) 89 | ) { 90 | hideLinkPreview(); 91 | return; 92 | } 93 | clearTimers(); 94 | 95 | const text = await fetch(href).then((x) => x.text()); 96 | if (currentHref !== href) return; 97 | 98 | showPreviewTimer = setTimeout(() => { 99 | if (currentHref !== href) return; 100 | const doc = new DOMParser().parseFromString(text, "text/html"); 101 | const content = ( 102 | doc.querySelector(".sl-markdown-content") as HTMLElement 103 | )?.outerHTML; 104 | tooltip.innerHTML = content; 105 | tooltip.style.display = "block"; 106 | let offsetTop = 0; 107 | if (hash !== "") { 108 | const heading = tooltip.querySelector(hash) as HTMLElement | null; 109 | if (heading) offsetTop = heading.offsetTop; 110 | } 111 | tooltip.scroll({ top: offsetTop, behavior: "instant" }); 112 | 113 | computePosition(target, tooltip, { 114 | middleware: [offset(10), autoPlacement()], 115 | }).then(({ x, y }) => { 116 | Object.assign(tooltip.style, { 117 | left: `${x}px`, 118 | top: `${y}px`, 119 | }); 120 | }); 121 | }, 400); 122 | } 123 | 124 | const events = [ 125 | ["mouseenter", showLinkPreview], 126 | ["mouseleave", hideLinkPreview], 127 | ["focus", showLinkPreview], 128 | ["blur", hideLinkPreview], 129 | ] as const; 130 | 131 | Array.from(elements).forEach((element) => { 132 | events.forEach(([event, listener]) => { 133 | element.addEventListener(event, listener); 134 | }); 135 | }); 136 | ``` 137 | 138 | 3. Add `LinkPreview` component 139 | 140 | ```astro 141 | // src/components/LinkPreview.astro 142 | 143 | 161 | 164 | ``` 165 | 166 | 4. Use `LinkPreview` component in the base layout 167 | 168 | 169 | 170 | ### Starlight specific code 171 | 172 | 173 | 174 | 1. Use `LinkPreview` component in the `PageFrame` 175 | 176 | ```astro {5, 14} 177 | // src/components/PageFrame.astro 178 | --- 179 | import Default from "@astrojs/starlight/components/PageFrame.astro"; 180 | import LinkPreview from "./LinkPreview.astro"; 181 | --- 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | ``` 191 | 192 | 2. Override `PageFrame` in Astro config 193 | 194 | ```js {6} 195 | // astro.config.mjs 196 | export default defineConfig({ 197 | integrations: [ 198 | starlight({ 199 | components: { 200 | PageFrame: "./src/components/PageFrame.astro", 201 | }, 202 | }), 203 | ], 204 | }); 205 | ``` 206 | 207 | 208 | 209 | ## Further improvements 210 | 211 | - [ ] Handle non-HTML links (images, PDFs) 212 | - [ ] Handle footnotes 213 | 214 | ## Reference implementations 215 | 216 | - https://github.com/OP-Engineering/link-preview-js 217 | - [mediawiki-extensions-Popups](https://github.com/wikimedia/mediawiki-extensions-Popups) 218 | - [wikipedia-preview](https://github.com/wikimedia/wikipedia-preview) 219 | - [quartz popover](https://github.com/jackyzha0/quartz/blob/v4/quartz/components/scripts/popover.inline.ts) 220 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/sidebar.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sidebar 3 | tags: [component] 4 | description: Starlight sidebar issues and ideas for improvements 5 | --- 6 | 7 | Starlight provides a sidebar, but there are some issues: 8 | 9 | - ~~It doesn't preserve state (scroll, open/closed submenus) on page switch~~ (fixed in v0.26.0) 10 | - It's not possible to have a root page for submenus 11 | 12 | There are some workarounds: 13 | 14 | - [starlight#971](https://github.com/withastro/starlight/discussions/971#discussioncomment-8396511) 15 | - [starlight#307](https://github.com/withastro/starlight/issues/307#issuecomment-1622619025) 16 | 17 | ## Starlight - Scroll Current Page into View 18 | 19 | **Note**: Use Starlight v0.26.0 or later instead. 20 | 21 | import { Steps } from "@astrojs/starlight/components"; 22 | 23 | 24 | 1. Create `Sidebar` component 25 | 26 | ```astro 27 | //src/components/Sidebar.astro 28 | --- 29 | import Default from "@astrojs/starlight/components/Sidebar.astro"; 30 | --- 31 | 32 | 33 | 34 | 64 | ``` 65 | 66 | 2. Configure Astro 67 | 68 | ```js {6} 69 | // astro.config.mjs 70 | export default defineConfig({ 71 | integrations: [ 72 | starlight({ 73 | components: { 74 | Sidebar: "./src/components/Sidebar.astro", 75 | }, 76 | }), 77 | ], 78 | }); 79 | ``` 80 | 81 | 82 | 83 | ## Alternative sidebars for Starlight 84 | 85 | Starlight provides configurable sidebar, which is good enough for documentation. For digital garden we may want to use different approach, for example: 86 | 87 | - [[alphabetical-index|alphabetical list]] 88 | - [[recently-changed-pages|articles grouped by date]] 89 | - [[tag-list|tag list or articles grouped by tags]] 90 | 91 | ### Alphabetical list 92 | 93 | ```astro 94 | // src/components/Sidebar.astro 95 | --- 96 | import Default from "@astrojs/starlight/components/Sidebar.astro"; 97 | 98 | import { getBrainDb } from "@braindb/astro"; 99 | import { isContent } from "@lib/braindb.mjs"; 100 | import type { Document } from "@braindb/core"; 101 | 102 | const firstChar = (str: string) => String.fromCodePoint(str.codePointAt(0)!); 103 | 104 | const docsByChar = new Map(); 105 | (await getBrainDb().documents()).forEach((doc) => { 106 | if (isContent(doc)) { 107 | const char = firstChar(doc.title()).toUpperCase(); 108 | docsByChar.set(char, docsByChar.get(char) || []); 109 | docsByChar.get(char)?.push(doc); 110 | } 111 | }); 112 | 113 | const comparator = new Intl.Collator("en"); 114 | const sidebarAlpha = [...docsByChar.keys()] 115 | .sort(comparator.compare) 116 | .map((char) => { 117 | let collapsed = true; 118 | return { 119 | type: "group", 120 | label: char, 121 | entries: docsByChar.get(char)?.map((doc) => { 122 | const isCurrent = doc.path() === `/${Astro.props.id}`; 123 | if (isCurrent) collapsed = false; 124 | return { 125 | type: "link", 126 | href: doc.url(), 127 | isCurrent, 128 | // @ts-ignore 129 | label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), 130 | // @ts-ignore 131 | badge: doc.frontmatter()?.["sidebar"]?.["badge"], 132 | // @ts-ignore 133 | attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, 134 | }; 135 | }), 136 | collapsed, 137 | badge: undefined, 138 | }; 139 | }) as Props["sidebar"]; 140 | 141 | sidebarAlpha.unshift(Astro.props.sidebar[0]); 142 | --- 143 | 144 | 145 | ``` 146 | 147 | ### Articles grouped by date 148 | 149 | ```astro 150 | // src/components/Sidebar.astro 151 | --- 152 | import Default from "@astrojs/starlight/components/Sidebar.astro"; 153 | 154 | import { getBrainDb } from "@braindb/astro"; 155 | import { isContent } from "@lib/braindb.mjs"; 156 | import type { Document } from "@braindb/core"; 157 | 158 | const docsByDate = new Map(); 159 | (await getBrainDb().documents({ sort: ["updated_at", "desc"] })).forEach((doc) => { 160 | if (isContent(doc)) { 161 | const date = doc.updatedAt().toISOString().split("T")[0]; 162 | docsByDate.set(date, docsByDate.get(date) || []); 163 | docsByDate.get(date)?.push(doc); 164 | } 165 | }); 166 | 167 | const sidebarDates = Array.from(docsByDate.keys()) 168 | .map((d) => [d, d.split("-").reverse().join(".")]) 169 | .map(([date, label]) => { 170 | let collapsed = true; 171 | return { 172 | type: "group", 173 | label, 174 | entries: docsByDate.get(date)?.map((doc) => { 175 | const isCurrent = doc.path() === `/${Astro.props.id}`; 176 | if (isCurrent) collapsed = false; 177 | return { 178 | type: "link", 179 | href: doc.url(), 180 | isCurrent, 181 | // @ts-ignore 182 | label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), 183 | // @ts-ignore 184 | badge: doc.frontmatter()?.["sidebar"]?.["badge"], 185 | // @ts-ignore 186 | attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, 187 | }; 188 | }), 189 | collapsed, 190 | badge: undefined, 191 | }; 192 | }) as Props["sidebar"]; 193 | 194 | sidebarDates.unshift(Astro.props.sidebar[0]); 195 | --- 196 | 197 | 198 | ``` 199 | 200 | ### Articles grouped by tags 201 | 202 | ```astro 203 | // src/components/Sidebar.astro 204 | --- 205 | import Default from "@astrojs/starlight/components/Sidebar.astro"; 206 | 207 | import { getBrainDb } from "@braindb/astro"; 208 | import { isContent } from "@lib/braindb.mjs"; 209 | import type { Document } from "@braindb/core"; 210 | 211 | const docsByTags = new Map(); 212 | (await getBrainDb().documents()).forEach((doc) => { 213 | if (isContent(doc)) { 214 | // @ts-expect-error 215 | doc.frontmatter().tags.forEach((tag: string) => { 216 | docsByTags.set(tag, docsByTags.get(tag) || []); 217 | docsByTags.get(tag)?.push(doc); 218 | }); 219 | } 220 | }); 221 | 222 | const comparator = new Intl.Collator("en"); 223 | const sidebarTags = [...docsByTags.keys()] 224 | .sort(comparator.compare) 225 | .map((tag) => { 226 | let collapsed = true; 227 | return { 228 | type: "group", 229 | label: `#${tag}`, 230 | entries: docsByTags.get(tag)?.map((doc) => { 231 | const isCurrent = doc.path() === `/${Astro.props.id}`; 232 | if (isCurrent) collapsed = false; 233 | return { 234 | type: "link", 235 | href: doc.url(), 236 | isCurrent, 237 | // @ts-ignore 238 | label: doc.frontmatter()?.["sidebar"]?.["label"] || doc.title(), 239 | // @ts-ignore 240 | badge: doc.frontmatter()?.["sidebar"]?.["badge"], 241 | // @ts-ignore 242 | attrs: doc.frontmatter()?.["sidebar"]?.["attrs"] || {}, 243 | }; 244 | }), 245 | collapsed, 246 | badge: undefined, 247 | }; 248 | }) as Props["sidebar"]; 249 | 250 | sidebarTags.unshift(Astro.props.sidebar[0]); 251 | --- 252 | 253 | 254 | ``` 255 | 256 | ## Multiple sidebars experiment 257 | 258 | ```astro 259 | // src/components/Sidebar.astro 260 | --- 261 | import { Tabs, TabItem } from "@astrojs/starlight/components"; 262 | // ... other code see above 263 | --- 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | ``` 277 | 278 | ### TODO 279 | 280 | - [ ] Render as partials and fetch from the server instead of prerendering, because there are too many DOM nodes. 281 | - [ ] Tabs should preserve state across navigations. 282 | - [ ] The scroll area should be inside the `TabItem`. 283 | -------------------------------------------------------------------------------- /packages/website/src/content/docs/recipes/metro-map-diagram/lisboa.output.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------