Skip to content

Ocean redesign + App Router migration - #52

Open
joelvarty wants to merge 81 commits into
mainfrom
rebuild/ocean
Open

Ocean redesign + App Router migration#52
joelvarty wants to merge 81 commits into
mainfrom
rebuild/ocean

Conversation

@joelvarty

Copy link
Copy Markdown
Member

What's in this branch

App Router migration (Next 16, Cache Components)

  • Pages Router → App Router with locale-based routing (app/[locale]/[...slug], default locale unprefixed via proxy.ts rewrite — pattern from demosite2025)
  • Cache Components ('use cache' + cacheTag + cacheLife) data layer in lib/cms/ with instant invalidation via the /api/revalidate webhook (revalidateTag(tag, 'max'))
  • draftMode()-based preview, JSON-LD (WebSite / TechArticle / BreadcrumbList), per-module server-component data fetching
  • npm replaces yarn

Ocean design system (T1–T2)

  • Ocean tokens (styles/tokens.css), class-based dark mode (data-theme), three-state theme control, no-flash head script
  • 9 ocean component models wired to React components; full legacy-module reskin (no purple, both themes AA)

Content phases (T3–T5, staged in CMS — publish only after this deploys)

  • Hub, /web-studio, /page-management, /ai flagship pages; Overview/Editors/Developers section reorders

Machine readability (T7)

  • /docs/llms.txt index for AI agents
  • Clean markdown per article: append .md to any article URL

Verification

  • npm run build green: 325 static pages, PPR, both themes smoke-tested
  • Preview deploys tested: routing, preview mode, webhook, robots, sitemap, search
  • Docs: AGENTS.md fully rewritten for the new stack; plan + progress in docs/rebuild-plan-2026.md

Publish sequence (after merge + production deploy)

Publish staged pages 2 / 59 / 60 / 61 (+ section landings 3 / 4 / 5) in the Agility UI — the live bundle must know the ocean components before the content goes live.

🤖 Generated with Claude Code

joelvarty and others added 25 commits July 15, 2026 14:22
…-zone tuning)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… detection

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…down

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ig quarantined, HOSTING.md contract)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…endering bug

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d MCP content

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e 0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t 9 (Phase 0)

- next 14.2 -> 16.2.10 (Turbopack build, 8.7s), react 18 -> 19.2.7
- @agility/nextjs 14 -> 16.0.8, @next/third-parties 14 -> 16, @agility/content-fetch to 2.x
- serverExternalPackages for applicationinsights (dynamic requires can't bundle)
- React 19 types: JSX.Element -> React.JSX.Element (Changelog.tsx)
- next lint removed in Next 16: migrate to ESLint 9 flat config (eslint.config.mjs,
  eslint-config-next/core-web-vitals native flat export); new react-hooks v7
  compiler rules demoted to warnings for legacy Pages Router components that the
  ocean rebuild replaces
- tsconfig auto-migrated by Next (moduleResolution: bundler, jsx: react-jsx)

Verified: production build passes; homepage, article, and 404/500 prerender;
headlessui v1 menus open correctly under React 19 in a real browser with no
console errors; lint green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e fronting CDN

Standard RFC 9213 header (public, s-maxage=60, stale-while-revalidate=86400)
scoped to non-API routes, per the HOSTING.md cache strategy — lets the Netlify
proxy (or any RFC-9213-compliant CDN) serve docs pages from its edge instead of
proxying every request to origin. Also pin outputFileTracingRoot to this repo.

Verified locally: header present on /docs/overview, absent on /docs/api/robots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ran the official @tailwindcss/upgrade tool: config inlined into styles/globals.css
as CSS-first @theme/@Utility (tailwind.config.js removed), PostCSS switched to
@tailwindcss/postcss, 19 component files migrated to v4 utility names. Removed
dead plugins: nightwind (dark mode was disabled; ocean tokens replace it) and
tailwindcss-nested-groups (Changelog's group-scope classes converted to native
named groups: group/scope + group-hover/scope:).

Verified: build passes; homepage and article pages visually unchanged in browser
(header, hero, cards, sidebar tree, prose, callouts, TOC rail); lint green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… script, fonts (Phase 1 / T1)

- styles/tokens.css: ocean tokens verbatim from the handoff (neutral scale,
  light + dark blocks on html[data-theme], radius, elevation, focus-ring pattern)
- @custom-variant dark wired to data-theme for Tailwind dark: utilities
- Three-state ThemeControl (light/dark/system) in the header; system mode
  tracks prefers-color-scheme live; persisted to localStorage
- No-flash inline script in _document applies persisted theme before paint
- Inder (400-only, per handoff gotcha) and Fira Mono added via next/font

Per T1's definition of done, no component consumes the tokens yet — the visual
reskin lands with the T2 component work. Verified in browser: toggle flips
data-theme and persists, system mode resolves correctly, aria-pressed tracks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contentful report assembled from partial verification (their client-rendered
docs stall non-browser fetchers — both research agents hung on fetches);
unverified items are marked. Synthesis locks in six Phase 3 hub-design
decisions and five consensus content investments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s, verified both themes

CMS side (instance 67bc73e6-u, all additive): content models FeatureCard (44),
LinkCard (45); component models PageHero (46), MediaHero (47), FeatureCardGroup
(48, nested cards), ArticleListSection (49, nested items), CodeBlock (50),
CalloutBlock (51), ThemeAwareImage (52). Test page 'ocean-test' (pageID 58,
staging, hidden from menu/sitemap) composes 5 of them on Main Template.

Code side: components/agility-pageModules/ocean/* — tokens-only styling
(no hex in components), per-accent card bar/ink CSS (yellow never sits as
text on light), ThemeAwareImage light/dark swap classes, OceanLink for ~/
site-root hrefs. Font aliases (--font/--serif/--mono) moved to the <main>
wrapper: custom properties resolve var() refs at the declaring node, so
:root aliases of next/font vars (defined on main) compute to invalid —
and Next 16 forbids next/font in _document, so html-level vars aren't an
option.

Verified in browser on /docs/ocean-test in dev (preview content): Inder and
Fira Mono resolve, hero/code/callout/list render in both themes, theme
toggle flips live. Known issue: save_page_model via MCP returns IDs but
models never materialize — new page models deferred (existing templates
work; see plan).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… link colors

Header: surface bg + ocean border, teal nav actives/hovers, tertiary-yellow
filled CTA and teal outline CTA (AA in both themes), tokenized mobile menu.
Preheader/marketing banner: warm dark neutral (n-900) with tertiary accent.
Footer: n-900/n-950 neutrals replacing purples (consistent in both themes).
Templates (Main/WithSidebarNav/Fullwidth): page canvas reads --bg/--text.
Globals: prose links and list markers to --primary, marketing accents to
--tertiary.

The chrome is now fully theme-aware; remaining light islands in dark mode are
the old CMS-composed hub modules (HeroCategories etc.) that T3 replaces, plus
the search input (part of the Cmd-K reskin).

Verified in browser in both themes on the homepage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y module tokens

- FeatureCardGroup/ArticleListSection: fetch nested children via
  getCustomInitialProps (page fetch uses expandAllContentLinks: false,
  so linked lists arrive as {referencename}, same as legacy modules)
- Tokenize kept hub modules for dark mode: ListofLinks (dark band ->
  n-900 like the footer), RightOrLeftAlignedLinks, ArticleListing
- custom-shadow now uses --elev-1 instead of purple-tinted rgb

Hub page (pageID 2) recomposed in CMS staging: PageHero + 3 flagship
FeatureCards (primary/secondary/tertiary) + Browse-by-role LinkCards,
keeping Concepts / Guides / APIs / Getting Help.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…no-op, dropdown mangling, page-model phantoms)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…PreviewBar

Single floating pill (Agility mark, copy link, edit-in-CMS, options) that
expands to an options panel — same design as the marketing site's
PreviewBar, on ocean tokens so it themes. Keeps the docs behaviors:
exitPreview route, manager deep link, Ctrl/Cmd+Q toggle in live mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uploaded to CDN at cdn.aglty.io/agility-cms-docs/docs-redesign/ and used
as PageHero media on /web-studio, /page-management, /ai (pages 59-61).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd revalidation webhook

Replaces the Pages Router with the App Router architecture modeled on
demosite2025 and the marketing site, updated to Next 16 best practices:

- app/[locale]/[...slug] catch-all with generateStaticParams over the
  Agility sitemap (all locales); default locale serves unprefixed URLs
  via a proxy.ts rewrite (middleware is deprecated in Next 16). Locales
  come from AGILITY_LOCALES — multi-language is a config change.
- Cache Components (cacheComponents: true): lib/cms data layer wraps
  every Agility fetch in 'use cache' + cacheTag/cacheLife using the
  reference tag scheme (agility-content/page/sitemap-flat/graphql-*).
  getAgilityPageProps replaced with our own cached composition.
- /api/revalidate webhook: Agility publish events -> revalidateTag(tag,
  'max') + revalidatePath for instant invalidation.
- draftMode() preview via /api/preview + /api/preview/exit; dev serves
  staging content; FORCE_PUBLISHED=1 tests published behavior locally.
- All getCustomInitialProps modules are now async server components
  fetching their own data; interactive UIs split into client components
  (SideBarNavClient, ChangelogClient). Header computes active nav from
  usePathname; Footer is a cached server component.
- JSON-LD (WebSite/TechArticle/BreadcrumbList) emitted in-body per the
  marketing site's getRichSnippet pattern; generateMetadata replaces
  HeadSEO with canonical + OG + metaHTML extraction.
- All pages/api routes ported to route handlers; Apollo removed from
  the render path (kept for search indexing); sitemap.xml + robots as
  handlers.

Gotchas fixed along the way (see AGENTS.md):
- proxy.ts matcher must list bare '/' explicitly or the home page skips
  the locale rewrite.
- applicationinsights must not initialize during next build — its OTel
  RandomIdGenerator's Math.random() aborts Cache Components prerenders.
- algoliasearch client creation is lazy (host shuffle uses Math.random).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full stack/routing/caching/data-flow documentation plus the complete CMS
model inventory (doc article containers + ocean redesign models) and the
Cache Components gotchas discovered during migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-mode coverage

Tokenizes every remaining legacy surface so PUBLISHED content renders
ocean-themed without any content changes:
- HeroCategories: purple band + white overlapping cards -> ocean hero +
  accent-barred category cards (rotating primary/secondary/tertiary)
- HeroHeading: purple section bands -> ocean hero on --bg
- Sidebar (SideBarNavClient), ArticleNav + feedback, Search input +
  results, ButtonDropdown, FilterBlock, ToggleSwitch, feedback dialog
- Article prose: body/heading/table/blockquote colors, inline + block
  code on --code-bg, list markers; dark-mode hljs token remap
  (lightfair is a light-only theme); EditorJS block components
- TextBlocksWithImages, SDKsFrameworks, RighOrLeftAlignedImageLinks,
  Changelog UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lopers)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- lib/cms-content/articleMarkdown.ts: serialize DocArticle to clean markdown
  (markdownContent near-verbatim with H1 handling; EditorJS blocks converted)
- app/api/article-md/[...slug]: markdown endpoint, reached via new proxy
  rewrite /{article-path}.md (path in URL, not query — basePath rewrite
  drops added query params)
- app/llms.txt: AI-agent index from the cached published sitemap; flagship
  entries appear automatically when those pages publish

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agilitycms-documentation-site Ready Ready Preview Aug 24, 2026 8:13pm

Request Review

- Warning/Info article blocks: yellow-*/blue-* -> ocean callout recipe
  (matches CalloutBlock color-mix styling in both themes)
- Table block: brightPurple + hardcoded #D7E2ED header -> tokens
- Dialog overlays: bg-opacity-* is a no-op in Tailwind v4 (scrims rendered
  solid) -> bg-black/50
- Search results header, Header icons, ToggleSwitch, ArticleNav scroll-spy
  classes, TextBlocksWithImages dots, image-link tile, footer RenderLink
  hover -> tokens
- Remove dead nprogress (JS was dropped in the App Router migration; only
  the purple CSS remained) and the legacy purple/gray @theme palette

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
joelvarty and others added 2 commits July 29, 2026 21:14
SideBarNav interpolated a container's reference name straight into a GraphQL
query. That only worked because every container happened to be alphanumeric —
`ManagementSDK-Articles` produced `{ managementsdk-articles (take: 250) {…} }`,
which is a GraphQL syntax error (hyphens are illegal in field names). The throw
propagated out of the server component, so every article in that section
rendered the error boundary ("This page couldn't load") instead of its content.

- Map the reference name to the field name Agility actually exposes: lowercased
  with non-alphanumerics folded to `_` (`managementsdk_articles`). Verified a
  no-op for all existing containers, which the read API already lowercases.
- Alias both selections (`articles:` / `sections:`) so response keys are stable
  regardless of the container name.
- Catch query failures and fall back to the category-only nav, so a nav problem
  can never blank the article body again.

Note: a GraphQL-valid name isn't sufficient on its own — hyphenated containers
also return 0 items from the GraphQL index even though REST lists them. The
Management SDK content was therefore moved to new hyphen-free containers
(ManagementSDKArticles / ManagementSDKSections); see the Phase 3 notes in
docs/content-refresh-plan-2026.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AGENTS.md was already fixed, but two active instruction files still claimed the
MCP can't publish:

- CLAUDE.md: said "cannot set workflow state, publish, or delete". It can
  (publish_content/unpublish_content, manage_content_workflow, publish_page/
  unpublish_page, delete_content_item) — but save_content_items still writes to
  Staging, so a save isn't live until published, and publishing is outward-facing.
- authoring skill: removed the leftover "the MCP can't publish or delete" step
  that contradicted the skill's own corrected section.
- rebuild-plan §4: logged a new MCP bug (initialize_media_upload double-prefixes
  the folder path) and noted the dropdown-mangling bug did not recur on a
  45-choice DropdownList; AGENTS.md's bug list re-synced to match.
- content-refresh-plan: Phase 1/2/4 progress, the bulk publish record, the
  publish-dependency lesson, and the Phase 3 findings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Management SDK section rendered for the first time today. Documents the
diagnosis (hyphenated container names are invalid GraphQL field names AND are
not indexed by the GraphQL API at all, while REST lists them fine), the fix
(new hyphen-free containers, slugs preserved so URLs don't move), the sidebar
hardening, and the remaining topic-merge work with a source-to-target map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the 8 published language-tabbed articles (with their JS/.NET source
pairs and tab-group counts), the Webhooks article that rescued content the
topic merge would have dropped, the QC method (every article re-rendered
through the site's unified pipeline to verify label-to-panel parity), and the
nav entry.

Also logs ~10 places where the JavaScript and .NET SDK docs disagree about the
same endpoint — naming, return shapes, parameter contracts, a deprecation
conflict, and example page sizes that exceed the documented cap. These were
reproduced faithfully in the docs rather than guessed at, and are written up
for the SDK teams to verify.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SDK was documented twice — once under /javascript, once under /dotNet — and
both copies stayed published when the consolidated language-tabbed set went live,
so 14 URLs now duplicate the canonical articles and compete with them in search.

Rather than misuse `archived` (whose banner says "no longer actively maintained",
which is untrue of freshly-merged content), add a `superseded` status:

- Banner wording follows the status: superseded pages say "This page has moved"
  and link the canonical guide; archived pages keep the unmaintained wording.
- Both statuses are `noindex` — for superseded that's the point, it stops the
  duplicate competing with its own canonical copy.
- URLs stay live, per the §4 convention: bookmarks and external links still work.
- 14 exact article paths mapped to their canonical successors. The matcher only
  matches a whole path or a `path/` prefix, so these had to be per-article — the
  duplicates sit inside otherwise-current /javascript and /dotNet sections.

`getArchivedEntry` is aliased to the new `getNoticeEntry` so existing callers
compile unchanged. Verified with 39 assertions over the real matcher, covering
the two failure modes that would have been damaging: noindexing the new
canonical articles, and dropping /javascript or /dotNet from the nav.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the `superseded` retirement of the 14 duplicate Management SDK copies
and closes out the phase. Notes the optional follow-up (move the section to
top-level /management-sdk with redirects) that was deferred to keep URLs stable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…I specs

Every API claim in the docs was hand-written with nothing tying it to the
contract. A spot check found four wrong claims in minutes: a PAT field
documented as required that isn't, a single-item save documented as returning an
array when the endpoint returns one integer, two parameter names that don't
exist in the spec at all, and examples paging at take:1000/5000 against a
default of 50. This makes that check repeatable.

check_drift.py pulls both specs (Management + Fetch), reads articles through the
preview API so unpublished fixes count, and grades findings:

  HIGH   endpoints absent from both specs; "X is required" where no schema
         marks X required; page sizes past a declared maximum
  MEDIUM parameter-shaped identifiers the specs never declare; page sizes far
         past the defaults where no maximum is declared
  INFO   coverage

Grading matters because the cheap heuristics do misfire — SDK Options fields
(retryCount) and non-Agility params (webAppName) are expected false positives,
and SKILL.md lists them so they aren't re-reported as bugs. It also documents
the blind spots honestly: unbackticked identifiers, free-form return-type prose,
and semantic drift are NOT detected, so a clean run means "no mechanical
contradiction found", not "the docs are correct".

Verified by re-discovering the known errors independently, including one the
Pages merge had only flagged as suspicious (getPageTemplateName is absent from
the spec), plus two page-size problems in articles I hadn't examined. Confirms
fixes too: the corrected PAT article no longer flags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checked the Phase 3 merge findings against the live Management API spec. It
settled several open JS-vs-.NET questions and found four claims we had already
published were wrong (all inherited from the pre-merge sources): expiryDate
documented as required, a single-item save documented as returning an array, two
parameter names that don't exist, and examples paging at 1000/5000.

Records what was corrected, what the spec confirmed (the locale-variant query
params, OAuth endpoints, PAT creation, the webhook payload), and one useful
finding: every ".NET SDK can't do this" gap is an SDK gap, not an API gap — the
REST endpoints all exist, so the documented workaround is sound.

Also notes the take-limit nuance honestly: the spec declares no maximum, so
"exceeds the cap" was itself an unverifiable claim; the real defect was examples
assuming a single request returned everything.

§6 now lists api-spec-drift alongside audit-framework-docs as the monthly
anti-drift pair.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…specs

The spec diff had a structural blind spot: it can't see claims the docs make
about the SDKs. That's where the real errors were.

check_sdk_drift.py reads ground truth from the published packages —
@agility/management-sdk (npm .d.ts) and Agility.Management.SDK (NuGet assembly
metadata via dnfile) — and flags:

  - install commands naming the assembly instead of the NuGet package
  - "not available in the JavaScript/.NET SDK" claims the surface refutes
  - sections attributed to the wrong SDK via their signature lines
  - parameter-level "X only" claims contradicted by the other signature
  - documented methods absent from both SDKs

It also prints a verified JS <-> .NET method matrix (--matrix), which is the
only trustworthy source for cross-SDK availability tables.

Precision was the hard part. Earlier passes over-fired by treating "only
documented for X" (a statement about our docs) as an availability claim, and by
widening the context window until it swept in unrelated method names. It now
scopes to the table cell or the note line, strips "use Y instead" hand-offs,
stops the backwards walk at headings, and skips anything ambiguous — a wrong
subject means "correcting" a sentence that was right. Validated by a negative
control against the pre-fix article wording: all four known errors detected.

.NET absence is read from assembly metadata rather than `strings`, because the
ECMA-335 string heap stores one name as a suffix of another (PublishContent
inside UnPublishContent), so absence looks proven when it isn't.

Also fixes the .spec-cache gitignore path, which pointed at the skill directory
while the scripts cache at the repo root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Publishing the corrected Management SDK articles pushed the spec checker from
2 HIGH to 3, so the new findings needed verifying rather than trusting. Two of
the three were the checker's fault:

  - "`required` (it's a `settings` entry)" was read as "settings is required".
    The pattern for "required ... `field`" matched when `required` was itself
    the backticked subject. Added a lookbehind.

  - `const base = '.../api/v1/instance'` was reported as an endpoint that
    doesn't exist. A strict prefix of real spec paths is a URL building block,
    not a claim, so it's now exempt.

Chasing the second uncovered a pre-existing hole in the same check: it tested
known paths with re.search, so any invented suffix on a real route
(.../locales/{id}/obliterate) was silently accepted. Both comparisons are
fullmatch now — the query-string fallback included, where the substring match
was also redundant since the query is already stripped.

test_checks.py pins all of it: 19 cases, both directions, flag-cases using the
verbatim pre-correction article wording. Tuning for precision is exactly how a
checker goes quiet, and "0 HIGH" is worthless without evidence it would still
speak up.

Remaining real finding: dotNetArticles/1405 still claims expiryDate is
required. That's one of the 14 superseded duplicates, noindex'd and bannered but
still serving the pre-correction claim to anyone arriving on a bookmark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re it

Two numbers had drifted apart. CSS parks a clicked heading 80px below the
viewport top (scroll-margin-top, clearing the sticky header); the scroll-spy
tested `scrollPos >= heading.offsetTop - 60`. After a click that reduces to
`scrollPos >= scrollPos + 20`, which is never true, so the clicked heading never
counted as reached and the highlight stayed one behind — on every heading except
the first.

The offset now lives in one place, `--heading-scroll-offset`, which the CSS uses
for scroll-margin-top and the component reads via getComputedStyle. They can't
diverge again, which is the actual bug.

Also fixed while in here:

  - offsetTop -> getBoundingClientRect().top. offsetTop is relative to the
    nearest positioned ancestor, and the article sits inside sticky/positioned
    wrappers, so it was never a dependable document coordinate.
  - window.onscroll = fn -> addEventListener. Direct assignment silently
    replaces any other scroll listener on the page.
  - rAF-throttled; every sync reads layout and scroll fires far more often than
    that is useful.
  - Active state is React state now, so aria-current actually tracks the current
    heading. It was previously applied by mutating classList, leaving
    aria-current permanently unset for screen readers.
  - hashchange listener: anchor clicks don't reliably emit scroll before paint.
  - Renders nothing when an article has no headings, instead of an empty
    "On this page" heading.
  - scroll-margin-top extended to h3/h4, which are valid anchor targets too.

Verified against the reported scenario: with the old rule, clicking headings
1-4 of 5 highlighted 0,1,2,3; with the new rule each click highlights itself,
and free-scrolling still tracks correctly.

Adds docs/screenshots-needed.md — the 7 screenshots the two locale articles want,
with capture standards and the original block ids so images land where the author
intended. Those placeholders blocked publishing for four months; logging them
here means prose ships and the images follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I read "last modified 2026-03-20" as abandoned, stripped the screenshot
placeholders from both articles and published them. They were active drafts and
were unpublished the same day.

Age is not ownership. The doc now says so at the top, names the owner, and the
capture checklist starts with "check with the author" instead of ending with
"publish". The screenshot specs are still useful; they are a reference, not
approval to touch the articles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both checkers read articles through the preview Fetch API, which is CDN-cached
and lags a save by minutes. It reported an article still carrying edits that had
already been reverted, and I relayed that as fact — nearly causing the revert to
be done twice.

The Management API (get_content_item) is uncached and authoritative. Recorded as
a blind spot so a stale read isn't mistaken for current state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joelvarty and others added 3 commits August 24, 2026 16:11
… type

Jina chose Concept 4 on 2026-08-21 — teal #028D83, blue #295BAC, CTA yellow
#FFCB28 — on Stone with Warm bone body copy. The neutrals needed no change:
Stone and Warm bone were already this site's defaults at the exact hexes. The
three brand hues were still the pre-2026 Ocean values.

The teal cannot do both of its jobs with one hex. As body-size copy on dark,
#028D83 reads 4.38 / 4.08 / 3.59 on bg / surface / raised — under the 4.5 bar
everywhere. So --primary stays her value for fills, rules and rings, and a new
--primary-text (#02A196, same hue and saturation 4% lighter) reads
5.58 / 5.19 / 4.57 and carries type and icons. Light needs no split. Every
text-(--primary) moved to text-(--primary-text); fills, borders and rings did
not. Two deliberate exceptions: FilterBlock's form-checkbox (the forms plugin
uses `color` for the checked box, so it is a fill) and the code-tab underline.

Blue is a background colour on dark — #295BAC as copy is 2.72:1. Dark feature
cards take --secondary-bright for both ink and the accent bar, since the fill
value is under even the 3:1 graphics bar on a card.

Also adds --on-primary, because the label on a teal fill flips by theme:
near-black on dark (4.75:1), white on light (5.29:1). This fixes a pre-existing
failure — the mega-menu's active pill was 3.74:1 in light using --on-color.

Prose list markers: the .prose li::before rules parsed but never painted (a
::before with no content generates no box), leaving @tailwindcss/typography's
cool-grey defaults — counters 3.70:1 on dark, bullets 1.28:1 on light. Replaced
with the --tw-prose-* variables the plugin actually reads.

Diagram sources recoloured per the same fill/type split, and #76716B (--n-500,
not a text token) doing fine-print duty at 3.04–3.70:1 became --faint. Every
text element in all three now clears AA. The live CDN copies still need
uploading — see the rollout section in docs/brand-palette-2026.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…there too

Two bugs with one root.

The on-this-page nav listed headings from a different page. ArticleNav scraped
document.querySelectorAll("#DynamicArticleDetails h2"), and during an App Router
client transition the outgoing page is still mounted — two of those subtrees
exist at once, both connected and visible — so the query merged both articles.

It also took up to 9 seconds to appear. It was built 100ms after hydration, so
it inherited however long the page took to hydrate, and DynamicArticleDetails
was "use client". That meant the whole render path ran twice: the server
produced the HTML, then the browser re-ran unified/remark/rehype over the
markdown and re-ran highlight.js over every code block to produce byte-identical
output, before hydrating ~1,000 highlighted nodes. Measured on the dev server,
the TOC appeared after 102ms on a 4KB article and 8,916ms on a 74KB one with 10
code blocks. Until hydration finished nothing on the page was interactive — nav,
search, theme toggle, all of it. The nav was just the visible symptom.

Headings are now derived on the server (lib/docs/renderArticleBody.ts, shared
with the article module via React cache() so the pipeline runs once per request)
and passed to ArticleNav as a prop, so the nav is in the SSR HTML and there is
nothing to scrape. Only the scroll-spy needs the client.

The body is now a server component. The three things that genuinely need the
browser are separate: ClassicModeSwitch (both bodies arrive as rendered server
nodes, so toggling is a visibility change), ArticleBodyEnhancer (code tabs and
inline-script re-execution — it takes a target id rather than wrapping the body,
so the article is not serialised a second time into the RSC payload), and the
scroll-spy. Two other modules were "use client" for no good reason and dragged
highlight.js in with them: BlockEditor had no hooks or state at all, and
ocean/CodeBlock highlighted in an effect — its copy button is now CopyCodeButton.

On /docs/developers/content-fetch-api: TOC 8,916ms -> 0ms, client JS
13,252KB -> 11,227KB (dev, unminified), 32 -> 27 chunks, and zero chunks
containing highlight.js or the remark/rehype pipeline. Verified: highlighting,
the Classic toggle, scroll-spy tracking, no duplicate heading ids across the two
bodies, and both the markdown and blocks paths.

Production is not measured — every number here is from the dev server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adopts the shell from Agility-Website-Nextjs-2026
(src/components/preview/preview-bar.client.tsx): a round button pinned to the
right edge that opens a centred modal with a status pill, rather than the old
left-edge vertical pill.

Pinned right because the old bar overlapped the sidebar nav — it was clipping
"Using Agility for Multiple Sites" and its neighbours at common widths.

Keeps the docs-only editor affordances the marketing panel has no need for:
Copy link, the Edit-in-CMS deep link, and the Ctrl/Cmd+Q toggle. Styled from
the ocean tokens rather than the marketing site's hardcoded greys, so it tracks
the brand palette and both themes, and using this site's sharp-corner radius.

Status pills are solid where they need attention and fixed in both themes:
near-black on yellow for Preview (12.78:1), white on blue for Development
(6.59:1), neutral outline for Live.

Replaces alert() and confirm() with in-modal UI — Copy link shows a "Link
copied" tick, exiting preview is a two-step inline confirm. No Enter Preview
action: /api/preview requires a valid agilitypreviewkey, so there is no keyless
way in from the UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant