Aller au contenu
← Retour aux projets

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, because remark-math runs 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:

  1. Visit /keystatic on the deployed site and follow its GitHub App setup flow.

  2. It gives you KEYSTATIC_GITHUB_CLIENT_ID, KEYSTATIC_GITHUB_CLIENT_SECRET, KEYSTATIC_SECRET and NEXT_PUBLIC_KEYSTATIC_GITHUB_APP_SLUG. Add all four to the Vercel project's environment variables.

  3. 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-motion must go through useStillness() — it reads the query via useSyncExternalStore so 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 at opacity: 0.
  • Base element styles belong in @layer base. Unlayered CSS outranks Tailwind's layered utilities, so an unlayered a { color: inherit } silently beats every text-* 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.

Nouvelle version disponible.