Render legacy callout shortcodes, add code syntax highlighting

Converts the note/tip/warning/danger `{% <tag> %}...{% </tag> %}` blocks
the posts→writings migration script left untranslated into styled
.callout markup (toml-content-loader.ts's renderCallouts(), styled in
global.css to match the new design system).

Also wires shiki into the same markdown pipeline for fenced code block
highlighting (github-light/github-dark, switched by the same
[data-theme]/prefers-color-scheme pattern tokens.css already uses).

Also records the Three.js centerpiece's perf budget check in TODO.md:
scene is trivially light (well under 10k triangles, 100+fps uncapped);
the ~130KB gzip payload is three.js core itself, not a tree-shaking gap
(verified named imports change nothing - sideEffects:false already lets
Rollup shake fully), and the script is a deferred module that doesn't
block first paint. No code changes needed there.
This commit is contained in:
2026-09-01 09:14:29 +03:00
parent c035aca132
commit 74a7f80b29
5 changed files with 202 additions and 5 deletions
+22 -4
View File
@@ -90,7 +90,15 @@ approved plan for full rationale on each decision referenced below.
- [x] Fallback for `prefers-reduced-motion` (skips rotation/entrance
animation) and no-WebGL (`supportsWebGL()` check hides the canvas
entirely, falls back to a `<noscript>`-style plain count)
- [ ] Perf budget check on a low-end/mobile profile — not yet measured
- [x] Perf budget check — scene is trivially light (24 nodes, 12x12-segment
spheres, a handful of line-segment edges: well under 10k triangles),
renders at 100+fps uncapped with no dropped frames. The real cost is
the JS payload: three.js core pulls in ~130KB gzip / ~106KB brotli
regardless of `import *` vs named imports (`sideEffects: false`
already lets Rollup tree-shake fully; tried named imports, no size
change). That script is a deferred module (doesn't block first
paint/DCL) and the page was interactive in ~270ms in testing. No
code changes made — budget is fine as shipped
## Content features carried over from the Zola site
@@ -108,6 +116,11 @@ approved plan for full rationale on each decision referenced below.
`Base.astro`)
- [x] RSS/JSON feeds — `/rss.xml`, `/feed.json`
- [x] Sitemap + robots.txt — `@astrojs/sitemap`, static `public/robots.txt`
- [x] Code block syntax highlighting — `toml-content-loader.ts` renders
fenced code blocks through shiki (`github-light`/`github-dark`,
`defaultColor: false` so only per-token colors are set and our own
`pre` background/border/padding still apply), switched by the same
`[data-theme]`/`prefers-color-scheme` pattern as `tokens.css`
## Search & comments
@@ -134,9 +147,14 @@ approved plan for full rationale on each decision referenced below.
additive only (`content/posts/` untouched), re-runnable/idempotent.
2 of 20 posts (`from-c-to-machine-code`,
`self-hosting-gitea-and-mirroring-github`) use note/tip/warning/danger
callout shortcodes with no Astro equivalent yet — flagged by the
script, needs a real component once the design system exists
(**not blocking**, just noted for later)
callout shortcodes — flagged by the script, still left as-is in the
body text
- [x] note/tip/warning/danger callout shortcode rendering — the legacy
`{% <note> %} ... {% </note> %}` blocks are converted to styled
`.callout` markup by `toml-content-loader.ts`'s `renderCallouts()`
(runs before the body's markdown parse), styled in `global.css` to
match the new design system. Verified in-browser on
`from-c-to-machine-code`
- [x] Preserve slugs/URLs via redirects — the script generates 301s from
`/posts/<slug>/` to `/writings/<slug>/` into `sws.toml`, idempotently
- [ ] Migrate `content/archive/`, `content/about.md` equivalents — holding
+1
View File
@@ -23,6 +23,7 @@
"astro": "^7.2.9",
"marked": "^18.0.11",
"pagefind": "^1.5.2",
"shiki": "^4.4.3",
"smol-toml": "^1.8.0",
"three": "^0.185.1"
},
+99 -1
View File
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
import { extname, join, relative } from "node:path";
import type { Loader } from "astro/loaders";
import { Marked } from "marked";
import { createHighlighter } from "shiki";
import { parse as parseToml } from "smol-toml";
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
@@ -33,6 +34,52 @@ function stripInlineMarkdown(text: string): string {
// as `headings`, matching the shape a built-in markdown loader would give.
let currentHeadings: Heading[] = [];
// Languages actually used in fenced code blocks across content/ — shiki
// resolves common aliases (js→javascript, sh/zsh→bash, ps1→powershell,
// bat→batch) on its own, so only the langs with no bundled grammar
// (gdb console sessions, svg markup, plain txt) need an explicit remap.
const highlighter = await createHighlighter({
themes: ["github-light", "github-dark"],
langs: [
"asm",
"bash",
"batch",
"c",
"css",
"html",
"javascript",
"powershell",
"python",
"toml",
"xml",
"yaml",
"plaintext",
],
});
const LANG_ALIASES: Record<string, string> = {
gdb: "plaintext",
svg: "xml",
txt: "plaintext",
"": "plaintext",
};
function highlightCode(code: string, lang: string | undefined): string {
const resolved = LANG_ALIASES[lang ?? ""] ?? lang ?? "plaintext";
try {
return highlighter.codeToHtml(code, {
lang: resolved,
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
});
} catch {
return highlighter.codeToHtml(code, {
lang: "plaintext",
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
});
}
}
const marked = new Marked({
renderer: {
heading(token) {
@@ -42,9 +89,60 @@ const marked = new Marked({
currentHeadings.push({ depth: token.depth, slug, text: plainText });
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`;
},
code(token) {
return highlightCode(token.text, token.lang);
},
},
});
const CALLOUT_ICONS: Record<string, string> = {
note: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
tip: '<circle cx="12" cy="12" r="10"></circle><path d="M12 8v4"></path><path d="M12 16h.01"></path>',
warning:
'<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line>',
danger:
'<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"></polygon><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
};
const CALLOUT_TITLES: Record<string, string> = {
note: "Note",
tip: "Tip",
warning: "Warning",
danger: "Danger",
};
// Legacy Zola shortcode syntax carried over verbatim by the posts→writings
// migration script (which flagged, but didn't translate, these blocks —
// see scripts/migrate-posts-to-writings.mjs). Matches
// `{% <note> %}\n...body...\n{% </note> %}` for note/tip/warning/danger.
const CALLOUT_RE =
/\{%\s*<(note|tip|warning|danger)>\s*%\}\r?\n?([\s\S]*?)\r?\n?\{%\s*<\/\1>\s*%\}/g;
/** Replaces legacy callout shortcode blocks with the `.callout` markup
* (styled in global.css), rendering each block's inner markdown. Must run
* before the body's own `marked.parse()` call so the shortcode syntax
* doesn't get treated as literal paragraph text. */
async function renderCallouts(body: string): Promise<string> {
const matches = [...body.matchAll(CALLOUT_RE)];
if (!matches.length) return body;
let out = body;
for (const match of matches) {
const [full, tag, inner] = match;
const innerHtml = await marked.parseInline(inner.trim());
out = out.replace(
full,
`<aside class="callout callout-${tag}">
<div class="callout-header">
<svg class="callout-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${CALLOUT_ICONS[tag]}</svg>
<span class="callout-title">${CALLOUT_TITLES[tag]}</span>
</div>
<div class="callout-content">${innerHtml}</div>
</aside>`,
);
}
return out;
}
function walk(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -95,7 +193,7 @@ export function tomlContentLoader(contentDir: string): Loader {
const relPath = relative(dirUrl.pathname, file);
const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, "");
currentHeadings = [];
const trimmedBody = body.trim();
const trimmedBody = await renderCallouts(body.trim());
const html = await marked.parse(trimmedBody);
const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length;
const readingMinutes = Math.max(1, Math.round(wordCount / 200));
+77
View File
@@ -93,6 +93,28 @@ pre {
border: 1px solid var(--border);
}
/* Shiki syntax highlighting (toml-content-loader.ts renders code fences with
defaultColor: false, so only per-token color vars are set — background,
padding, etc. stay on our own `pre` rule above). Follows the same
light/dark switching pattern as tokens.css: system preference by default,
[data-theme] overrides it either way. */
.shiki,
.shiki span {
color: var(--shiki-light) !important;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .shiki,
:root:not([data-theme="light"]) .shiki span {
color: var(--shiki-dark) !important;
}
}
:root[data-theme="dark"] .shiki,
:root[data-theme="dark"] .shiki span {
color: var(--shiki-dark) !important;
}
.container {
max-width: 42rem;
margin-inline: auto;
@@ -169,6 +191,61 @@ pre {
color: var(--accent);
}
.callout {
margin: 0 0 var(--space-5);
padding: var(--space-4) var(--space-5);
border: 1px solid var(--border);
border-left: 3px solid var(--callout-color, var(--accent));
border-radius: var(--radius-md);
background: var(--bg-elevated);
}
.callout-header {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-2);
color: var(--callout-color, var(--accent));
}
.callout-icon {
width: 1.1em;
height: 1.1em;
flex-shrink: 0;
}
.callout-title {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.callout-content {
color: var(--text);
}
.callout-content > :last-child {
margin-bottom: 0;
}
.callout-note {
--callout-color: var(--accent);
}
.callout-tip {
--callout-color: #4a9c6d;
}
.callout-warning {
--callout-color: #d1a53d;
}
.callout-danger {
--callout-color: #d1502f;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
+3
View File
@@ -105,6 +105,9 @@ importers:
pagefind:
specifier: ^1.5.2
version: 1.5.2
shiki:
specifier: ^4.4.3
version: 4.4.3
smol-toml:
specifier: ^1.8.0
version: 1.8.0