Repository navigation
Step 2 — Markdoc + Shiki + content collections #4
Description
Summary
Wire up Markdoc rendering for the site repo: a markdoc.config.mjs that highlights fenced code with Shiki using the project's arcp-light / arcp-dark brand themes (dual-theme, class-based), and a src/content.config.ts defining two Astro content collections (docs for synced SDK/spec markdown, home for the single frontmatter-driven home entry). After this phase, a Markdoc page renders with on-brand syntax highlighting and the home frontmatter is schema-validated.
Why / context
The target stack is Astro 5.x + @astrojs/markdoc (Markdoc has no Vue renderer, which is why we left Nuxt). This phase establishes the content-rendering substrate that Steps 3–6 build on: routing/nav/TOC consume the docs collection, the home page consumes the home data collection, and every code block on the site flows through the Shiki config defined here. The "Inkwell" design system is sacred — the Shiki token colors are WCAG-tuned hardcoded hex and must be carried over byte-for-byte. Big-picture spec: /Users/nficano/code/arpc/site/docs/markdoc-migration.md (see "Step 2 — Markdoc + Shiki + content collections").
Reference files (read these first)
/Users/nficano/code/arpc/www/shiki-arcp.mjs— the two brand theme objects. Copy this file verbatim intosite(e.g.site/shiki-arcp.mjs). It exportsarcpLightandarcpDark(named exports built viabuild('arcp-light', light)/build('arcp-dark', dark)). Do not re-derive or "clean up" the hex values — see gotchas./Users/nficano/code/arpc/www/content.config.ts— the@nuxt/contentcollection definitions. Port the Zod schema forhome(thelinkSchema+seo/header/hero/sections[]/cta/footershape). Drop thedefineContentConfig/defineCollection(type: 'page') Nuxt wrappers and thedocssource-glob shape — re-express both as Astro collections. Notezis imported from@nuxt/contentthere; in Astro import it fromastro:content(orastro/zod)./Users/nficano/code/arpc/www/nuxt.config.ts— the Nuxt Shiki wiring to mirror. The canonical 17-lang list and the dual-theme mapping live here (content.build.markdown.highlightat ~L52-71 andmdc.highlightat ~L37-51). Copy thethemes/theme/langsintent; drop everything Nuxt-specific (modules,mdc.highlight.noApiRoute,content.watch, FontAwesomebuild.transpile,ogImage,sitemap). Note the explicit comment at L62-66 thatlightis set on purpose so it is not defaulted tomaterial-theme-lighter(pastel, ~2.2:1 contrast).
Tasks
- Add the Markdoc integration to
astro.config.*(@astrojs/markdocinintegrations). If Step 0 already added it, confirm it is present. - Copy
www/shiki-arcp.mjsverbatim tosite/shiki-arcp.mjs. Verify it still exportsarcpLightandarcpDark. Do not alter any hex. - Create
site/markdoc.config.mjs(or.ts) usingdefineMarkdocConfigfrom@astrojs/markdoc/config. - Register Shiki via
@astrojs/markdoc/shikias a Markdocextendsentry. Configure it with:themes(dual-theme, class-based):{ light: arcpLight, dark: arcpDark }, imported from./shiki-arcp.mjs. This produces dual-theme output that switches on the.darkclass, not the OSprefers-color-schememedia query — matching today's class-based dark mode (the.dark/.lightclasses on<html>from Step 1). Do not use the single-themeform (it would lock one palette).langs— the exact 17 langs fromnuxt.config.ts, in this order:json, bash, typescript, javascript, python, rust, go, yaml, toml, http, csharp, fsharp, java, kotlin, php, ruby, swift. (httpis not bundled by default in some Shiki setups — verify it loads; load it as a langAlias/extra grammar if needed.)- Disable Shiki's default
wraponly if it differs from current behavior; otherwise leave defaults.
- Ensure Markdoc's default nodes render correctly: headings (with slugged anchor
ids — TOC and deep links in Step 3 depend on slugs matching today's), paragraphs, ordered/unordered lists, GFM tables (used in 474 files), blockquotes (764 files), links, inline code, and fenced code (→ Shiki). Tables/GFM may need to be enabled in the Markdoc/markdown options — verify a table actually renders. - Confirm heading slugs match the current site's slugging (used for
#anchordeep links across thousands of synced internal links). If Astro/Markdoc's default slugger diverges, override the heading node to match. - Create
site/src/content.config.tswith two collections defined viadefineCollectionfromastro:content. - Define the
docscollection using the Markdocloader(glob({ pattern: '**/*.{md,mdoc}', base: ... })) pointing at the synced content dir. Per the spec, Step 4 writes synced docs tosrc/content/docs/<lang>/..., so glob that base. No required schema — docs carry no frontmatter (titles come from the first# H1). If you add an optionaltitle(Step 4 may inject one), make it.optional()so unmigrated files still validate. - Define the
homecollection as a data collection: a single-file loader (e.g.file('src/content/home.json')or aglobforhome.md) with the ported Zodschema. - Port the
homeZod schema fromwww/content.config.tsexactly. Reproduce:linkSchema={ label: string, to?: string, href?: string, icon?: string, external?: boolean }.seo: { title, description }.header: { name, tags: string[] }.hero: { eyebrow, headline, lede }.sections: Array<{ id, number, kicker, kind: enum['prose','grid','list','columns','code'], body?, items?: Array<{title?, description?, label?, index?}>, columns?: Array<{heading, tone: enum['positive','neutral'], items: Array<{label, note?}>}>, code?: { title, lang, caption?, source }, diagram?: { outer: {label, tags: string[]}, inner: {label, tags: string[]}, aside } }>.cta: { label, to }.footer: { line1, line2, contact: linkSchema }.
- Add a minimal smoke-test fixture: a
.mdoc/.mdfile in the docs base containing an# H1, a GFM table, a blockquote, and a fenced```typescriptblock; render it via a throwaway/temporary route and confirm the Shiki output carries both light and dark token spans/vars. - Run
pnpm build(orastro build) and confirm the collections type-check and the Markdoc config loads without errors.
Details & gotchas
- The Shiki hex is WCAG-tuned — never edit it.
shiki-arcp.mjs's header comments document that light tokens are checked against the#fcfaf4paper panel (≥4.5:1 text, ≥3:1 punctuation) and dark tokens against the#1a1813panel. Examples:keyword: '#9f3416'(vermilion-700),string: '#4f6b3a'(olive--str),number: '#9a4a1f'(burnt amber--key),func: '#7d2911', withcommentitalic at~5.2:1. Thebuild()helper emits a full TextMatesettingsarray (scopes likeentity.name.function,support.type.property-name,meta.object-literal.key,entity.name.tag) pluscolors['editor.background'/'editor.foreground']. Copy the file as-is. - Do not let Shiki default the light theme.
nuxt.config.tsL62-66 explicitly setslight: arcpLightprecisely so the highlighter does not fall back tomaterial-theme-lighter(pastel, ~2.2:1) underprefers-color-scheme. Carry the same explicitness into the Astrothemesmap. - Dual-theme must be class-based, not media-query. Today's dark mode is class-based (
.dark/.lighton<html>, established in Step 1). Configure the Shiki integration's dual-theme so the generated CSS variables key off the.darkclass rather than@media (prefers-color-scheme: dark). If the integration emits media-query-scoped variables by default, switch it to class-based (thedefaultColor: false/ class-based dual-theme mode). - No custom doc tags needed. The spec verifies zero
::component/ MDC component usage in docs prose, so you do not need to author a callout/tabs/cards Markdoc tag library here. The only raw-HTML case (diagram<img>passthrough) is explicitly Step 5, not this ticket — Markdoc sanitizes raw HTML by default and that is fine for now. @nuxt/content→ Astro shape changes.defineContentConfig,defineCollection({ type: 'page', source }), and importingzfrom@nuxt/contentare all Nuxt-isms. In Astro:defineCollection+ aloader(fromastro/loaders:glob/file) +zfromastro:content. The Nuxtdocscollection usedsource: { include: '**/*.md', exclude: ['home.md'] }; in Astro,homeis a separate data collection so the docs glob just targetssrc/content/docs/**.homeis data, not page. It is frontmatter-driven (the home page renders bespoke sections from these fields, Step 6), so it belongs in adata/fileloader collection, not the Markdocdocscollection. Four fields (hero.lede, sectionbody, stepdescription, codesource) are markdown strings rendered at build time later — they remain plainz.string()here..mdcvs.md/.mdoc. Astro's Markdoc loader keys off the file extension. The synced docs are.md; if any SDK emits.mdc(Step 4 handles renaming), make sure the docs glob pattern and any loader extension assumption agree. Glob**/*.{md,mdoc}is the safe target.httpgrammar. Of the 17 langs,httpis the most likely to be missing from a given Shiki bundle — confirm it highlights an```httpblock rather than rendering plain.
Acceptance criteria
-
site/markdoc.config.mjsexists, importsarcpLight/arcpDarkfrom a copiedshiki-arcp.mjs, and registers@astrojs/markdoc/shikiwiththemes: { light: arcpLight, dark: arcpDark }and all 17 langs. - Shiki output is dual-theme, class-based: a fenced code block produces both light and dark token colors that switch on the
.darkclass (verified by toggling the class, not the OS theme). - Code token colors match the current site exactly (spot-check
keyword=#9f3416light /#f0612fdark,string=#4f6b3a/#9bbd76) — i.e.shiki-arcp.mjsis byte-identical towww's. -
src/content.config.tsdefines adocsMarkdoc-loader collection (no required frontmatter) and ahomedata collection with the fully ported Zod schema; both type-check. - A smoke-test Markdoc file renders:
# H1(with a slugid), a GFM table, a blockquote, and a Shiki-highlighted```typescriptblock. -
pnpm buildcompletes with no Markdoc/collection/Shiki errors.
Dependencies & out of scope
- Depends on Step 0 (Astro scaffold +
@astrojs/markdocinstalled) and Step 1 (design tokens + the class-based.dark/.lightdark-mode mechanism this Shiki config keys off). Thedocscollection's base dir (src/content/docs/<lang>) is finalized by Step 4 (docs-sync) — this ticket can point the glob there ahead of content arriving, using the smoke-test fixture to validate. - Out of scope: routing /
[...slug].astro/ sidebar nav / TOC / breadcrumb (Step 3); the docs-sync pipeline (Step 4); diagram<img>raw-HTML passthrough and any HTML-whitelisting (Step 5); home-page rendering and the<MDC>-equivalent inline-markdown rendering of the four runtime strings (Step 6); SEO/OG/sitemap (Step 7). No custom Markdoc tag library (none is used in docs prose). No search.
Part of #1 · Full spec: docs/markdoc-migration.md
Activity
Step 2 — Markdoc + Shiki + content collections