Website
#Divergent Labs — landing page
Neurodivergence-aligned reasoning research. A single statically-prerendered page built from the Substrate ASCII design.
#Stack
| Concern | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) + React 19 | Page prerenders to static HTML; only the animated leaves ship as client components |
| Language | TypeScript (strict) | Canvas renderers and figure geometry are typed end to end |
| Styling | Tailwind CSS v4 | Design tokens live in @theme in src/app/globals.css |
| Scroll | Lenis | Drives real window scroll, so anchors, useScroll and IntersectionObserver keep working |
| Reveals, parallax, counters | Motion | whileInView, useScroll, spring-smoothed parallax |
| SVG choreography, ticker | GSAP + @gsap/react |
Line-draw + looping pulses on Fig. 2; gsap.ticker is the single rAF loop for every canvas |
#Commands
npm run dev
npm run build
npm run lint
#Layout
content/posts/ blog posts as .mdx (this is the CMS)
content-collections.ts frontmatter schema + MDX pipeline
keystatic.config.ts dashboard schema — keep in sync with mdx.tsx
src/
app/ layout (fonts, metadata), globals.css (tokens), page.tsx
blog/ (index, [slug], feed.xml)
keystatic/ + api/keystatic/ (dashboard)
sitemap.ts, robots.ts, llms.txt, opengraph-image.tsx
components/ one file per section, plus Reveal / Parallax / canvas primitives
mdx.tsx maps MDX elements to the site's typography
JsonLd.tsx emits Organization + BlogPosting structured data
lib/
art.ts line-art renderers (pure: ctx, w, h, seconds)
prism.ts Fig. 1 - serial vs. divergent search, a function of progress 0..1
ascii.ts 64x40 ASCII density portrait
content.ts landing page copy and figure data
posts.ts published/draft filtering, date formatting
site.ts canonical URL + org facts, used by every metadata route
useStillness.ts hydration-safe prefers-reduced-motion
#SEO / AEO
Generated automatically, no dashboard needed — each derives from the post
frontmatter and src/lib/site.ts:
| Route | Purpose |
|---|---|
/sitemap.xml |
All pages + published posts |
/robots.txt |
Allows crawlers, hides /keystatic and /api |
/blog/feed.xml |
RSS |
/llms.txt |
Plain-text site summary for AI crawlers (llmstxt.org) |
/opengraph-image |
Social card for the home page |
/blog/<slug>/opengraph-image |
Per-post social card, generated from the title |
JSON-LD structured data is emitted as Organization site-wide and
BlogPosting on each post. This is the highest-leverage AEO lever: answer
engines read it directly rather than inferring facts from prose.
Set NEXT_PUBLIC_SITE_URL in Vercel so preview deployments emit their own URLs
instead of the production domain.
#Publishing a post
Add a .mdx file to content/posts/. The filename becomes the slug. Commit
and push — Vercel rebuilds and the post is prerendered as static HTML.
--- title: Your title summary: One or two sentences, used in listings, meta tags and RSS. date: 2026-08-01 kind: Research note tags: [evals] draft: false ---
title, summary and date are required and validated at build time — a
malformed post fails the build rather than shipping broken. draft: true posts
render locally but are excluded from the index, RSS and sitemap.
Beyond standard Markdown you get:
- Math —
<Math formula="\rho" />inline,<MathBlock formula="..." />for a centred line. See the warning below before reaching for$…$. - Code — fenced blocks with Shiki highlighting.
<Callout>— bordered aside with crop marks.<Figure art="graph" caption="..." />— embeds the site's own animated canvas figures (graph,wave,cyl,lens,sphere) inside a post.
New components go in src/components/mdx.tsx and are then available in every
post without an import. If a component should also be usable from the dashboard,
register it in keystatic.config.ts too — as inline() if it belongs inside a
sentence, block() if it stands alone, wrapper() if it takes children.
Getting that wrong fails with paragraph has unexpected children.
Do not use
$…$or$$…$$math. It renders fine, becauseremark-mathruns before the MDX expression parser — but Keystatic's editor has no such plugin, so\frac{a}{b}is handed to acorn as a JavaScript expression and the entry becomes impossible to open in the dashboard (Could not parse expression with acorn).<Math formula="..." />passes the LaTeX as a quoted attribute, which has no braces for either parser to trip over. The formula cannot contain a double quote.
#Editing in the browser
npm run dev, then open /keystatic. It reads
and writes the same .mdx files — there is no database, and no content lives
anywhere but this repo.
Saving reformats the file cosmetically (YAML folded strings, aligned table pipes, indented component children). That is expected and lossless; content survives a round-trip unchanged.
#Making it work on the deployed site
keystatic.config.ts ships with storage: { kind: "local" }, which writes to
disk and therefore only works locally — Vercel's filesystem is read-only at
runtime. To edit from a browser in production, commit the repo to GitHub, then:
-
Visit
/keystaticon the deployed site and follow its GitHub App setup flow. -
It gives you
KEYSTATIC_GITHUB_CLIENT_ID,KEYSTATIC_GITHUB_CLIENT_SECRET,KEYSTATIC_SECRETandNEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG. Add all four to the Vercel project's environment variables. -
Change storage to:
storage: { kind: "github", repo: "UditAkhourii/<repo-name>" }
Saves then land as commits on GitHub, which triggers a Vercel rebuild. Only step 1 needs a browser; nothing else about the site changes.
#Notes for future edits
- Copy and figure data live in
src/lib/content.ts. Sections read from it; only long-form prose is inlined in JSX. - Every canvas figure is a pure function of time, so it can be looped, frozen for reduced motion, or scrubbed without touching the component.
- Reduced motion is load-bearing. All content sits behind reveal
animations, so anything reading
prefers-reduced-motionmust go throughuseStillness()— it reads the query viauseSyncExternalStoreso the hydrating render matches the server. Branching on the media query during hydration changes the markup React is matching, and React 19 does not repair mismatched attributes: the page would stay stuck atopacity: 0. - Base element styles belong in
@layer base. Unlayered CSS outranks Tailwind's layered utilities, so an unlayereda { color: inherit }silently beats everytext-*utility applied to a link. - Canvases re-sync their backing store to the CSS box on a slow cadence as well as via ResizeObserver, so a late-settling layout cannot leave a figure rendered at a stale resolution.
#Known advisories
npm audit reports 12 high-severity advisories, all transitive: minimatch /
brace-expansion under eslint and eslint-config-next (dev only), and
postcss / sharp pinned by next itself. There is no non-breaking fix —
npm audit fix --force downgrades Next.js — so they are left in place.