This document holds two playbooks for agents migrating content here:
- App Router + article-grid — moving an interactive article into
app/(main)/(article-grid)/with MDX, metadata, and colocated demos (see the next major section). - Stitches → Tailwind — replacing
@stitches/reactwith Tailwind in article demos and components (see Stitches → Tailwind migration).
Use the section that matches the task. The legacy in-app /_stories browser and *.stories.tsx pipeline were removed from this repo; do not reintroduce them when migrating articles.
How to add or move a post to the App Router under the article-grid layout (pattern proven with magic-motion; references keys-in-framer-motion, database).
Create a segment at app/(main)/(article-grid)/<slug>/:
| File | Role |
|---|---|
layout.tsx |
export const metadata (title, description, authors, twitter, openGraph with url: https://nan.fyi/<slug>). Render ArticleTitle with the same title/description, then {children}. |
page.mdx |
Article body: imports + prose + embedded components. No YAML frontmatter — metadata lives only in layout.tsx. |
_components/ |
Post-specific interactive demos, colocated with the page (same idea as keys-in-framer-motion / database / magic-motion). |
Parent shell: app/(main)/(article-grid)/layout.tsx supplies the grid shell, Navbar, and GridSizeProvider.
In the route’s layout.tsx, import ArticleTitle from ../article-title.
Do not import ArticleTitle from how-computers-talk/header.tsx. That file re-exports the title helper in the same module as PageHeader, whose imports pull in client diagram code (Connectors, etc.). Loading that module while Next collects layout configuration / RSC for the segment can throw (e.g. createContext-style failures). article-title.tsx is server-only markup.
Check: rg 'ArticleTitle.*how-computers-talk/header' on new layouts should return nothing.
"use client"on each interactive demo.tsxunder_components/that uses hooks, XState,motion/ Framer Motion, browser APIs, etc.- If
page.mdxstays a server component, any shared component it imports that behaves like a client component (hooks, Stitches + React context, etc.) may need"use client"on that shared module, or the client boundary must move to a child. - Function props cannot be serialized from server MDX into client children. Patterns such as
<Demo from={(w, c) => ({ ... })} />in MDX will fail at build/prerender. Fix: move that block into a dedicated client component that defines the callbacks inside the file (example:TransformOriginSection.tsx).
The MDX toolchain may not treat Quiz.Option as defined even when Quiz.Option is assigned on the Quiz object. Fix: use named exports from components/Quiz/Quiz.tsx — QuizQuestion, QuizOption, QuizOptions, QuizTip, QuizSpoiler — and write <QuizOption> in MDX instead of <Quiz.Option>. Keep Quiz as the root wrapper.
Prefer Wide (~/components/mdx/Wide) over FullWidth for figures inside article-grid content so width and padding align with the grid.
If a demo imports another post’s code (e.g. TokenList from tokenizer), use a stable path such as ~/content/tokenizer/components/TokenList until that post is migrated. If server MDX imports it, ensure that module is safe for the boundary (often "use client" on the shared file).
pages/[content].tsx and lib/content.server.ts still serve remaining content/**/index.mdx posts via _dist-content produced by pnpm build:content (Babel). Do not remove build:content from dev / build until every such post is migrated off the Pages router or CONTENT_FOLDER is repointed.
pnpm build- Open
/<slug>and spot-check interactive blocks and layout chrome. - Confirm
ArticleTitleis only imported fromarticle-title.tsxin route layouts.
Many article demos use ~/components/Visualizer (ToggleButton, IconButton, PlayButton, UndoButton, etc.). Those re-use shared components/Button.tsx under the hood. When changing control styling, prefer updating Button / Visualizer wrappers rather than forking one-off buttons in the post.
The following sections summarize how the magic-motion article demos were migrated from @stitches/react to Tailwind. Use them when migrating other routes or components for styling only.
- Remove
styled,css,keyframes, anddarkThemeusage from the target scope. - Prefer Tailwind utility classes and
cn()for conditional styling. - Keep Framer Motion (
motion.*,layout,animate,transition) where it implements interactive demos—not as a replacement for every Stitches style, but do not strip motion-driven behavior unless the product asks for it.
- Merge classes with
lib/cn.ts:cn=clsx+tailwind-merge. Use it whenever classes are conditional or passed from props. - Colors:
tailwind.config.jsalready spreads Radix palettes (gray,blue, etc.). Map Stitches tokens like$blue8→blue8,$gray11→gray11(typicallybg-*,text-*,border-*,fill-*,stroke-*). - Radii: Stitches
radii.base(6px) aligns with Tailwindrounded-mdin practice; confirm visually when porting.
- Light mode only: Drop
[.darkTheme &]blocks anddarkThemeimports. Do not add Tailwinddark:variants or extend Tailwind with dark palettes unless the site-wide theme story changes. - No Stitches/CSS keyframe animations: Remove Stitches
keyframesand CSSanimation*properties; use a static final look (e.g.opacity-100). Do not remove Framer Motionanimate/layout/transitionunless the task explicitly says to kill motion demos. - Box shadow: Where Stitches used
boxShadow: "$sm", use Tailwindshadow-sm. For SVG filters that used Stitches drop shadows, usedrop-shadow-sm(orfilter-nonewhen removing shadow).
| Stitches | Tailwind / React |
|---|---|
styled("div", { ... }) |
<div className={cn(...)} /> |
styled(motion.div, { ... }) |
<motion.div className={cn(...)} /> |
variants: { foo: { true: { ... } } } |
cn(base, foo && "...") or small helpers |
$space$8, $4 |
p-8, gap-4, p-4 (Stitches space scale matches Tailwind spacing in most cases—spot-check rem values) |
@md in Stitches |
Tailwind md: (both use 768px here) |
boxShadow: "$sm" |
shadow-sm |
SVG filter: drop-shadow(...) |
drop-shadow-sm or filter-none |
Compound selectors (&:after, &:before) |
Tailwind after:* / before:* with content-[''] where needed, or a thin @layer rule in app/styles.css if the markup becomes unmaintainable |
css={{ ... }} prop (Stitches on components) |
style={{ ... }} and/or className |
- Leaf / shared primitives first (things imported by many demos): small presentational components, rulers, shared SVG shapes, etc.
- Feature components that only compose primitives.
- Grep gate: from the target folder,
stitches/~/stitches.configshould disappear for files in scope.
- Tailwind
fill-*/stroke-*/stroke-2work on SVG elements when applied viaclassName. - Prefer SVG attributes (
r,rx,x,y) for geometry; don’t rely on Tailwind forr—setr={6}(or similar) on<circle>/<rect>. - Biome may require accessible SVGs: for decorative diagrams, either add
role="img"+aria-label="..."or a proper<title>—match what we did undermagic-motionafter migration.
styled(motion.x)becomesmotion.x+className; ref types stay on the DOM element (SVGLineElement, etc.).- When replacing Stitches
css={{ width, height }}on a host component, merge intostyleor explicit Tailwind if sizes are fixed. useLayoutEffectwithout a dependency array runs every render (legacy pattern in at least one demo). Preserve behavior unless you intentionally fix the effect model.
- Search:
rg stitches app/(path)/(or the target tree)—no hits in migrated files. - Lint:
pnpm exec biome check <path>. - Build:
pnpm run build(Next may skip fulltsc—fix obvious type errors locally).
- Global
stitches.config.tsand unrelated packages still on Stitches (e.g.components/Visualizer, MDX helpers) unless the migration ticket explicitly includes them.
- Pages router
pages/_app.tsxmapsnext-themesdark todarkTheme.classNamefrom Stitches, not the string"dark". App-router layouts may not wrapThemeProviderthe same way. If you reintroduce dark mode later, align TailwinddarkModewith whatever class actually lands on<html>—that is separate from “delete dark branches” migrations.