├── 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 | [](https://starlight.astro.build) [](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 |
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 |
20 |
Backlinks
21 |
22 | {links.map((x) => (
23 | -
24 | {x.title()}
25 |
26 | ))}
27 |
28 |
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 |
34 |
Backlinks
35 |
36 | {links.map((x) => (
37 | -
38 | {x.title()}
39 |
40 | ))}
41 |
42 |
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 | 
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 |
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 `${label}`;
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 ``;
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 |
--------------------------------------------------------------------------------