Skip to content

[variant — do not merge] Document-level translation (alternative to field-level i18n)#62

Draft
damianrosellen1 wants to merge 49 commits into
mainfrom
variant/document-level
Draft

[variant — do not merge] Document-level translation (alternative to field-level i18n)#62
damianrosellen1 wants to merge 49 commits into
mainfrom
variant/document-level

Conversation

@damianrosellen1

@damianrosellen1 damianrosellen1 commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Important

This PR is intentionally a draft and must never be merged. It exists to document a long-lived parallel variant of the boilerplate that uses a different internationalisation strategy. Use it as a side-by-side reference; keep main as the canonical, field-level variant.

TL;DR

This branch (variant/document-level) is a second flavour of the boilerplate. It swaps the field-level i18n approach used on main (sanity-plugin-internationalized-array) for document-level translation via the official Sanity plugin @sanity/document-internationalization. Pick the flavour that matches your editorial model — both are production-shaped.

References #18 (now closed). The variant branch lives on as a permanent alternative — this PR is a documentation-only side-by-side and is intentionally never merged.


The two strategies in one paragraph each

Field-level (main). Every translatable field stores all languages inside the same document as an array of { language, value } entries. Editors see language tabs inside each field. The web app fetches the document once and runs a JavaScript resolver to pick the right entry for the current locale. Queries are locale-agnostic; the document is the unit of editing.

Document-level (this variant). Every language is its own document with a language field. Sibling translations are connected by a translation.metadata document auto-created by the plugin. Editors see a "Translations" toolbar that switches between language variants. The web app filters at the document level (language == $locale) and consumes plain string / Portable Text fields — no runtime resolver.

Neither approach is universally better. The plugin's docs lay out the trade-offs; this PR exists so you can compare both side by side in real code.


Decision guide for adopters

Use main (field-level) when:

  • You want all translations of one piece of content edited in one place.
  • Translations are short, mostly UI-shaped, and rarely diverge structurally between languages.
  • Editors are non-technical and the language-tabs UI is welcome.
  • You're comfortable with a small JavaScript fallback resolver shipping per-render.

Use this variant (document-level) when:

  • Translations are first-class content pieces and may diverge structurally (different modules, different slug, different SEO).
  • You want GROQ queries to do the locale matching (no JS resolver in the hot path).
  • You plan to integrate a translation workflow (TMS, machine-translation hand-off) that operates per-document.
  • You prefer Sanity's official "Translations" toolbar UX over per-field language tabs.

What changed vs main

1. Plugin swap

  • Removed: internationalizedArray({…}) plugin block from studio/sanity.config.ts.
  • Added: documentInternationalization({ supportedLanguages, schemaTypes, languageField }). supportedLanguages is loaded from the siteLanguageSettings singleton at session start (same source as on main, identical fallback chain).
  • The peer sanity-plugin-internationalized-array stays installed transitively — the document-level plugin uses it internally for its translation.metadata.translations array, but our content schemas no longer touch it.

2. Schemas

All six translatable document types — home, page, errorSettings, siteSettings, siteNav, siteCookieBanner — gain a hidden, read-only language field. Field types convert:

Before (main) After (this branch)
internationalizedArrayString string
internationalizedArrayRichText richText
internationalizedArrayRichTextMedia richTextMedia

The firstLocalizedLabel preview helper is deleted (titles are plain strings now). The slug field on page gains an isUniqueLocaleAgnostic validator so the same slug can exist for different language variants (/about in both en and de).

3. Studio structure & Presentation

  • Singletons that previously used S.document().documentId("home") now use S.documentTypeList("home") so editors see one row per language variant; the plugin's Translations toolbar handles switching inside each document.
  • Presentation mainDocuments registers four routes — /, /:locale, /:slug, /:locale/:slug — with language filters on the locale-prefixed variants.
  • locationsResolver projects language alongside slug.current and emits /{language}/{slug} URLs; the web proxy redirects the default-locale prefix to the canonical unprefixed URL.

4. Web GROQ

  • homeQuery and pageBySlugQuery filter at the document level with language == $locale and project plain title (no array wrapper).
  • The recursive buildRichTextMediaQuery(depth) function is replaced with a static depth-2 inline literal, eliminating the typegen blocker.
  • internationalizedRichTextArrayField(fieldName) helper deleted; errorSettings body fields project plain Portable Text.
  • Sitemap queries carry language so each language variant is emitted as a separate URL.

5. Web types

  • web/sanity/utils/sanityLocalizedText.ts (pickLocalizedString, parseLocalizedText, pickLocalizedPortableTextBlocks, resolveLocalizedPortableTextDeep and the Intl*Entry shapes) is deleted entirely.
  • Hand-maintained module/document types switch from Intl*Entry[] to plain string / PortableTextBlock[].
  • The generated web/sanity/sanity.types.gen.ts now resolves seven queries cleanly — including HomeQueryResult and PageBySlugQueryResult, which main cannot currently type because of the recursive fragment. The typed-GROQ pipeline is therefore fully complete on this variant, vs. hybrid on main.

6. Fetch wrappers & routing

  • Every per-document fetch in fetchSanityData.ts and cachedSanityQuery.ts takes a required locale: string parameter. fetchSiteLanguageSettings stays unparameterised — it is the locale registry itself.
  • Cache keys / tags become locale-specific: home-{locale}, page-{slug}-{locale}.
  • /api/revalidate/route.ts validates language on the webhook payload and emits locale-aware tags.
  • Route pages thread locale through; generateStaticParams for [locale]/[slug] uses each page document's own language instead of a cartesian locale × slug product.

7. Components

Every pickLocalized* call is gone — ModuleText, ModulesRenderer, LocaleNotFoundContent, and resolveSanityMetadata access module.title, data.title, errorSettings.notFoundBody directly. The siteLocale prop drops off the module renderer chain.


Bonus side-effect

Because the typegen-blocking recursive query is gone on this variant, the typed-GROQ pipeline is fully wired — all seven exported queries that the web layer fetches produce generated *QueryResult types. On main the pipeline is hybrid (five queries typed, the module-bearing ones hand-typed). It's the cleanest demonstration of why doc-level i18n composes more naturally with the modern Sanity tooling.


Status

Check Result
pnpm -r run typecheck green (studio + web)
pnpm --filter studio run generate + diff guard idempotent
pnpm --filter web run generate + diff guard idempotent
pnpm --filter studio run build green
pnpm --filter web run build green
pnpm lint green (one pre-existing useOptionalChain warning, unchanged from main)

Known follow-ups (not part of this PR)

  • Hreflang sitemap alternates are not emitted on this variant. Cross-locale relationships live in the plugin's translation.metadata document; the sitemap currently emits one row per language variant without alternates.languages. Re-adding hreflang means joining translation.metadata from sitemapPagesQuery and emitting the language map — a focused follow-up PR, not a blocker.
  • Real-data migration for adopters with existing field-level content is out of scope. The plugin ships a migrateToLanguageField helper for the related v5 transition; documenting a complete content migration is a separate task.

How to try it locally

git fetch origin
git checkout variant/document-level
pnpm install
pnpm -r run typecheck   # green
pnpm studio:dev         # see per-language singletons + Translations toolbar
pnpm web:dev            # /about and /de/about resolve to their own documents

damianrosellen1 and others added 3 commits May 28, 2026 17:21
- Install @sanity/document-internationalization (^6.2.1).
- studio/sanity.config.ts: replace internationalizedArray({...}) with
  documentInternationalization({ supportedLanguages, schemaTypes,
  languageField }).
- studio/config/sync/internationalizedArrayLanguages.ts renamed to
  supportedLanguages.ts (loader signature identical, plus a try/catch
  fallback that addresses the earlier robustness gap).
- All 9 schemas migrated: page, home, errorSettings, siteSettings,
  siteNav, siteCookieBanner gain a hidden `language` field; module
  objects (text/carousel/contentRefs) get plain string/richText/
  richTextMedia field types (no per-field i18n wrapper). Previews use
  plain string access; firstLocalizedLabel helper deleted.

NOTE: this is an intermediate commit on the long-lived variant branch.
typecheck/build are intentionally NOT green here — the queries, web
types, fetch wrappers, components, structure items, and presentation
resolver still assume field-level i18n and will be migrated in the
following commits.
Studio side of the document-level migration is now internally consistent
(studio typecheck green; web side migrates in the next etappe).

Structure items for the five singletons (home, siteSettings, siteNav,
errorSettings, siteCookieBanner) switch from a fixed-id S.document() to
S.documentTypeList(<type>) so the desk shows one row per language
variant of each singleton; the plugin's Translations toolbar handles
sibling switching inside each document.

Slug uniqueness is now language-aware: `studio/utils/validateSlug.ts`
exports `isUniqueLocaleAgnostic` which scopes uniqueness per (_type,
language). `studio/schemas/documents/page.ts` wires it into the slug
field's `options.isUnique`, so /about can exist in en AND de.

Presentation:
- `resolve.ts` `presentationMainDocuments` now registers four routes:
  `/` and `/:slug` (default-locale, no language constraint), plus
  `/:locale` and `/:locale/:slug` (`language == $locale`). Presentation
  prefers the most specific match, so per-locale routes win when the
  iframe URL carries a locale segment.
- `locationsResolver.ts` SLUG_QUERY now selects `language` alongside
  `slug.current` and emits a `/{language}/{slug}` URL. The web proxy
  redirects default-locale prefixes to the canonical unprefixed URL.

schema.json and sanity.types.gen.ts regenerated under the new
schemas (52 types incl. `translation.metadata` from the plugin).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web is now consistent with the Studio's document-level model:

QUERIES
- `pages/home.ts` and `pages/page.ts` filter by `language == $locale`
  and project plain `title` (no field-level array wrapper). Both wrap
  in `defineQuery` — the typegen-blocking `richTextMediaQuery` recursion
  is gone, so the full pipeline can be typed now.
- `snippets/settings.ts`: every settings/nav query takes `$locale` and
  filters at the document level; `internationalizedRichTextArrayField`
  helper deleted; error-body fields project plain `richText` blocks.
- `snippets/sitemap.ts`: rows carry the document's `language` so each
  language variant is emitted as a separate URL.
- `components/text/richTextMedia.ts`: rewrite from the recursive
  `buildRichTextMediaQuery(depth)` function to a static depth-2 inline
  literal (typegen-friendly, idempotent).
- `components/modules/text.ts`: flatten body — no `{language, value}`
  wrapper.

TYPES
- `web/sanity/utils/sanityLocalizedText.ts` deleted; the utils barrel
  drops its re-exports of `parseLocalizedText`, `pickLocalizedString`,
  `pickLocalizedPortableTextBlocks`, `resolveLocalizedPortableTextDeep`
  and the `Intl*Entry` shapes.
- Hand-written types in `web/sanity/types/{pages,errorSettings,modules/*}`
  switch from `Intl*Entry[]` to plain `string`/`PortableTextBlock[]`,
  and gain a top-level `language` field where the doc carries one.
- `web/sanity/sanity.types.gen.ts` regenerates with 7 typed queries
  (HomeQueryResult and PageBySlugQueryResult now resolve cleanly — the
  full-pipeline bonus promised in the plan).

FETCH WRAPPERS / ROUTES
- `fetchSanityData.ts`: every per-document wrapper takes `locale: string`
  (`fetchHomeDocument`, `fetchPageBySlug`, `fetchErrorSettings`,
  `fetchSiteSettingsTitle`, `fetchSettingsSeoFallback`, `fetchSiteNavMenus`).
  `fetchSiteLanguageSettings` stays unparameterised — it is the locale
  registry itself.
- `cachedSanityQuery.ts`: `cachedPageDocumentBySlug(slug, locale)` and
  `cachedHomeDocument(locale)`; cache keys/tags become
  `page-{slug}-{locale}` / `home-{locale}`.
- `/api/revalidate/route.ts`: payload validation accepts `language`; the
  emitted tags include it for `home` and `page`.
- Route pages (`[locale]/page.tsx`, `[locale]/[slug]/page.tsx`,
  `not-found.tsx`, `sitemap.ts`) thread `locale` through every fetch.
- `generateStaticParams` for `[slug]` now uses each page document's
  declared `language` instead of the previous cartesian locale × slug
  product — only the slugs that actually exist for a language are pre-
  rendered.

COMPONENTS
- `ModuleText`, `ModulesRenderer`, `LocaleNotFoundContent`,
  `resolveSanityMetadata` access `module.title`/`data.title` directly;
  no more `pickLocalized*` calls. The `siteLocale` prop drops off the
  module renderer chain.

VERIFICATION
- `pnpm -r run typecheck`: green on studio AND web.
- `pnpm --filter studio run generate` + `--filter web run generate`:
  both idempotent against the committed artifacts.
- `pnpm --filter studio run build` and `pnpm --filter web run build`:
  green.
- `pnpm lint`: clean (only the pre-existing `useOptionalChain` warning
  on `RichTextMedia.tsx`).

The branch is now functionally complete. Issue #18 remains open as
the tracking issue for the variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MCLWallet added 4 commits May 28, 2026 18:07
… resolution

Updated the dataset resolution logic to exclusively use SANITY_STUDIO_PROJECT_ID, removing the fallback to NEXT_PUBLIC_SANITY_PROJECT_ID in both the resolveStudioDatasetAsync function and getSanityStudioProjectId function. This change simplifies the project ID retrieval process.
Consolidated the project ID retrieval logic in both the resolveStudioDatasetAsync and getSanityStudioProjectId functions by removing unnecessary line breaks. This enhances code readability without altering functionality.
…dability

Streamlined the JSX structure in the MediaImage component by eliminating redundant line breaks, enhancing code clarity without affecting functionality.
@netlify

netlify Bot commented May 28, 2026

Copy link
Copy Markdown

Deploy Preview for next-sanity-boilerplate-test ready!

Name Link
🔨 Latest commit 39292c0
🔍 Latest deploy log https://app.netlify.com/projects/next-sanity-boilerplate-test/deploys/6a2011e25ebd70000812ed28
😎 Deploy Preview https://deploy-preview-62--next-sanity-boilerplate-test.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

damianrosellen1 and others added 18 commits May 28, 2026 18:44
…mbs, autoplay)

Adds CMS-driven carousel behavior fields (loop, showThumbnails, showNavDots,
autoplay, autoplayDelayMs) and a production ModuleCarousel renderer using
embla-carousel-react + embla-carousel-autoplay. Replaces the dev-only
placeholder in ModulesRenderer and supports embeds in RichTextMedia.

Co-authored-by: Cursor <cursoragent@cursor.com>
…hydration mismatch

The boot script synchronously added `.img-loaded` to images already complete
in the browser cache, racing React 19 streaming hydration and producing a
"tree hydrated but attributes didn't match" warning on every page that ships
a non-priority Sanity image. Wrapping the class mutation in
`requestAnimationFrame` lets React reconcile the SSR markup first while still
keeping the fade-in transition imperceptible.

Co-authored-by: Cursor <cursoragent@cursor.com>
…o fix hydration

`syncSanityProjectId` previously read `SANITY_STUDIO_PROJECT_ID`, which Next
does not inline into the client bundle. SSR therefore built transformed Sanity
image URLs (`?w=…&auto=format&q=85`) while the hydrating client had no project
id and fell back to bare `image.asset.url`, producing a hydration mismatch on
every Sanity image.

Read `NEXT_PUBLIC_SANITY_PROJECT_ID` exclusively (and drop the equivalent
server-only `SANITY_STUDIO_DATASET` branch from `syncDataset`) so server and
client always compute identical URLs. Document the new env var requirement in
`.env.example`.

Co-authored-by: Cursor <cursoragent@cursor.com>
…mismatch

Adding the `img-loaded` class from the inline boot script raced React 19's
streaming hydration on cached images and produced "tree hydrated but
attributes didn't match" warnings. `requestAnimationFrame` was not enough
to push the DOM mutation past hydration reliably.

Move the lazy fade-in inside `MediaImage`: a `useRef` + `useEffect` flips a
`loaded` state once the image's `load` event fires (or immediately if the
image is already complete) and React renders the class — no more DOM /
hydration race. Strip the image-handling section from the boot script —
theme + `js-enabled` stay so the CSS opacity gate keeps working.

Co-authored-by: Cursor <cursoragent@cursor.com>
Updated `siteNavQuery` and `siteNavMenusQuery` to utilize a new `siteNavByLocale` query, which retrieves the active locale's `siteNav` document while falling back to a legacy document if no locale-specific document exists. This change improves internationalization handling in the navigation structure.
`web/.env` previously needed both `SANITY_STUDIO_PROJECT_ID` (server / Studio
convention) and `NEXT_PUBLIC_SANITY_PROJECT_ID` (so Client Components could
build matching Sanity image URLs). Same value, two names — confusing and
error-prone (forgetting one half of the pair causes a hydration mismatch).

Use Next's `next.config.ts` `env` map to inline `SANITY_STUDIO_PROJECT_ID`
and `SANITY_STUDIO_DATASET` into the client bundle. `sanitySyncConfig.ts`
now reads the Studio-named vars directly. `.env.example` drops the public
aliases. One env var, one name, identical SSR / CSR Sanity URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
… at opacity:0

`useEffect` only set `loaded=true` synchronously when both `el.complete` and
`naturalWidth > 0` were true. For images that errored before React's passive
effect attached its listeners, the load/error events had already fired and
were lost — `loaded` never flipped, the `img-loaded` class was never added
and the broken image stayed invisible (opacity:0) forever.

Drop the `naturalWidth > 0` guard. Both successful and failed loads need to
reveal the `<img>` (the alt text matters for failed ones); the running
listeners still cover the in-flight case.

Co-authored-by: Cursor <cursoragent@cursor.com>
…a-sanity

Add a `dataAttr` helper that composes `next-sanity`'s `createDataAttribute`
with the resolved `projectId` / `dataset` and the Studio URL, so callers only
need to pass the field path (`{ id, type, path }`). Used in `ModulesRenderer`
to wrap each module with `<div data-sanity={...}>` — Presentation tool can
now reverse-map a rendered module to its `modules[_key=="…"]` GROQ slot and
jump the Studio cursor into the corresponding field.

Skips the attribute when a module has no `_key` (legacy data) — without a
stable key the overlay would target the wrong array slot.

Co-authored-by: Cursor <cursoragent@cursor.com>
Silent failures of the SanityLive socket made it hard to tell whether live
content was syncing during draft mode. Pass an `onError` handler that emits
the error plus the request context (`includeDrafts`, `waitFor`) to
`console.error` — surfaces issues in DevTools without changing behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replaced the inline boot script with a new DocumentBootScript component that sets the theme based on localStorage and adds a 'js-enabled' class to the <html> element. This change enhances the structure and maintainability of the layout while ensuring the theme is applied before the first paint. Updated related comments in animations.css to reflect this change.
…sRenderer

- Introduced dynamic import for ModuleCarousel in ModulesRenderer to optimize loading.
- Updated RichTextMedia to utilize LinkMark from linkResolver, simplifying link management.
- Refactored LinkMark component to improve link resolution and rendering logic.

These changes improve performance and maintainability of link handling across components.
…ering

Introduced a new component, ModulesRendererClient, which wraps server-rendered modules and manages their reordering based on updates from Sanity Visual Editing. This component optimizes client-side rendering while ensuring that newly added modules appear after a refetch. The implementation enhances the user experience by providing instant feedback for reordering and deletion actions.
…ering

Introduced a new component, ModulesRendererClient, which wraps server-rendered modules and manages their reordering based on updates from Sanity Visual Editing. This component optimizes client-side rendering while ensuring that newly added modules appear after a refetch. The implementation enhances the user experience by providing instant feedback for reordering and deletion actions.
`<SanityLive />` is rendered from the server root layout, and Next 16 /
Turbopack rejects passing a function literal as a prop across the RSC
boundary. Extract `handleSanityLiveError` into a "use client" module so
it becomes a client reference the layout can safely import and pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the document-level translation branch, where each locale is its
own Sanity document. The Presentation overlay should jump editors to the
modules array as a whole rather than into individual field paths.

- dataAttr helper: drop 'path' from the Required pick so doc-level call
  sites are expressible at the type level. Runtime still rejects empty
  paths, so the shallowest valid scope here is the top-level field name.
- ModulesRenderer: compute one container-scoped data-sanity attribute
  ('modules' path) and pass it to ModulesRendererClient as a prop instead
  of attaching per-module field paths.
- ModulesRendererClient: accept optional containerSanityAttr and apply it
  on the outer flex container, leaving the per-module wrappers attribute-
  free.

The main (field-level i18n) branch keeps per-module deep paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the Header component to conditionally display a language switcher based on the number of available languages. Refactored menu entries to exclude the language switch option when not needed. Improved layout by grouping the language switcher and theme toggle for better UI consistency across mobile and desktop views.
…eader

Updated the Header component to enhance the readability of the LanguageSwitch component by formatting its props across multiple lines. This change aims to improve code clarity without altering functionality.
damianrosellen1 added a commit that referenced this pull request May 28, 2026
Restructure and tighten the root README for publication while keeping
every existing piece of accurate technical content.

What changed:
- Stronger lead: one-line tagline + a row of shields.io badges for
  Next, React, Sanity, TypeScript, Tailwind, Biome, license. No emoji.
- New "Two flavours" section right under the lead — surfaces the
  variant/document-level branch and links to PR #62 (the permanent
  side-by-side comparison and decision guide).
- "What you get" reflows the previous dense bullet sprawl into themed
  paragraphs (Next.js app, Studio, Media, Revalidation, SEO, Hardened
  defaults, Typed GROQ, Tailwind v4). Each block reads like a feature
  card, not a checklist.
- Quickstart split into clean steps instead of one bash block with
  inline numbered comments.
- "Architecture in brief" replaces the long "Core APIs & modules"
  tables; depth moves into the per-folder READMEs (already linked).
- New "License" footer; the "Requirements" block moves to the bottom
  where it belongs (you don't read it before you're sold on the repo).
- Fixed an orphan link to `packages/sanity-dataset-resolve/README.md`
  (file doesn't exist; resolver is documented inline in `src/index.ts`).
- The strip-readmes utility gets one explicit mention so consumers know
  the documentation density is opt-in.

Net result: 271 → 213 lines, 0 emoji, all links resolve.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolls out the public-release README from #63 to this branch, adapting the
branch-specific sections for document-level i18n:

- "Two flavours" table marks this branch as the current one
- Studio description points at the Translations toolbar
  (@sanity/document-internationalization) instead of per-field language tabs
- "Typed GROQ pipeline" notes full typegen coverage on this branch
- Get-started flow uses the Translations toolbar + per-locale slugs
- "Architecture → i18n" rewritten for document-per-locale + `language == $locale`
  GROQ filtering (no runtime resolver)

Everything else (stack, env, scripts, tooling, deploy, going-deeper) matches
the main README verbatim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
damianrosellen1 and others added 10 commits May 29, 2026 10:52
Makes the light/dark theme toggle an optional, editor-curated navigation
element — mirroring the nav.languageSwitch pattern. The toggle renders only
where an editor places the new `nav.themeToggle` block in Navigation → Main
Menu, via MainMenuItems; the always-on standalone toggles are removed from the
header chrome.

- New Studio object `nav.themeToggle` (registered + added to siteNav mainMenu)
- GROQ snippet, `nav.ts` types, and `navHref` resolver handle the new block
- Regenerated schema.json + typegen artifacts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d drop typography-clamp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document-level i18n schemas hid the `language` field with no
`initialValue`, so newly created documents (including freshly opened
singletons) were stored without a language and disappeared from
`language == $locale` queries until an editor set it manually. Set
`initialValue: "en"` on the `language` field of every translatable
document type: home, page, errorSettings, siteNav, siteSettings,
siteCookieBanner. The document-internationalization plugin's "Add
translation" flow still overrides this with the target locale; only
the bare "Create new" flow benefits.
Wires the Sanity-driven cookie banner into the boilerplate: lightweight
layout query + fetcher, client components under web/src/components/cookies,
and an `open-cookie-preferences` linkFunction handled in NavItem. The CSS
maps `--cc-*` tokens onto the boilerplate's `--color-*` semantic tokens so
the banner inherits light/dark mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Cookie consent (optional)" entry to the "What you get" section and
removes the `letter-spacing` declarations from every `@utility` block in
`typography.css` plus the matching three lines in the Studio Portable Text
previews. The `--letter-spacing-*` tokens stay in place for callers that
still need them; `blockquote p` keeps `letter-spacing: inherit`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the repo `next-sanity-boilerplate` → `next-sanity-starter` and
updates every in-repo reference: README headings + prose, root
`package.json` (name + description), Studio Sanity title, the strip-readmes
workflow commit message, the CI build comment, and the inline comments in
`web/next.config.ts`, `web/src/app/robots.ts`, and the fonts README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralize Portable Text vertical rhythm under `.rich-text` in
typography.css and strip margin utilities from RichTextMedia so spacing
stays consistent across rich-text renderers.

Fixes #68

Co-authored-by: Cursor <cursoragent@cursor.com>
Switch internal/external/function link titles from a GROQ coalesce on
`reference->title` to raw `title` + `resolvedReference.title`, then resolve
in app code via `resolveLinkLabel`. Handles both i18n-array nav titles and
plain string document titles so siteNav (singleton, field-level i18n) and
doc-level page titles share one code path. Adds preliminary work/project
routes to linkResolver + navHref.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brings the variant/document-level branch in sync with the main branch's
recent features (dark-mode toggle, multi-slide carousel, work/projects,
module.contentRefs, module structure refactor) while keeping document-
level internationalization as the canonical i18n pattern.

Highlights:
- Colors/typography/rich-text token updates (incl. --color-error fix and
  rich-text.css for Portable Text spacing)
- Toggle-only dark mode (ThemeContext, ThemeToggle, DocumentBootScript)
- Multi-slide carousel (schema, queries, types, viewport/slide layout)
  with arrow + dot controls beneath the viewport
- Modules folder refactor: shared moduleStyles, relocated
  ModuleCarousel into src/components/modules, barrel exports
- Work/Project/ProjectCategory document types adapted to document-level
  i18n (plain `title`, `language` field, locale-filtered GROQ)
- `module.contentRefs` adapted for document-level i18n (language-filtered
  references, plain-string titles/categories)
- Plain-string link titles (no internationalizedArray plugin needed on
  this branch); resolveLinkLabel falls back to referenced doc titles
- Studio Presentation routes for /work and /work/[slug] localized via
  language-aware locationsResolver and main-documents

Co-authored-by: Cursor <cursoragent@cursor.com>
Port main updates to variant/document-level
@netlify

netlify Bot commented Jun 3, 2026

Copy link
Copy Markdown

Deploy Preview for bef-next-sanity-starter ready!

Name Link
🔨 Latest commit 297899c
🔍 Latest deploy log https://app.netlify.com/projects/bef-next-sanity-starter/deploys/6a304fab18a8580008a9507c
😎 Deploy Preview https://deploy-preview-62--bef-next-sanity-starter.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

damianrosellen1 and others added 13 commits June 4, 2026 12:08
…specific tweaks)

Ports the canonical AGENTS.md + per-subtree CLAUDE.md + Cursor rules +
Copilot instructions from main. Branch-specific notes in AGENTS.md
already account for the variant's reduced renderer set, contentRefs
schema simplification, document-level language field, and the missing
modules/index.ts barrel.

Variant-only tweak: web/src/components/modules/CLAUDE.md notes that
ModulesRenderer imports module components directly and that re-creating
an index.ts barrel would be a dead file on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ails to variant/document-level

Mirrors the additions made on `main`:
- @repo/scaffold-module — `pnpm gen:module <Name>`. The components/modules
  barrel patch is marked optional, so the script gracefully skips it on
  this branch (no barrel exists; ModulesRenderer imports directly).
- @repo/check-wiring — `pnpm check:wiring`. Branch-aware by design:
  treats missing barrel as a skip and accepts Module<Name>Placeholder
  fallbacks in ModulesRenderer (covers ModuleContentRefs on this branch).
- Strip-readmes: root README.md is preserved.
- CI: wiring drift gate after typecheck.
- AGENTS.md: full CLI command reference (root/web/studio); CLAUDE.md
  files trimmed and aligned to Claude Code best practices (concise,
  @-imports, YOU MUST / IMPORTANT emphasis on the critical rules).
- web/src/components/modules/CLAUDE.md: variant-specific note that the
  generator skips the barrel patch and that ContentRefs is currently a
  placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit hook reformatted after staging. No behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(tooling): agent guardrails + gen:module + check:wiring (variant)
Dependabot reads its config from the default branch only, so this
file on `variant/document-level` is functionally ignored — but
keeping it diverged from main causes merge conflicts on every
`main → variant` sync and is misleading to read.

Bring it byte-identical to the canonical config on main (introduced
in #88), which now fans out updates to both branches via two
`updates:` entries with explicit `target-branch` fields.

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

chore(deps): mirror dependabot.yml from main (config-only)
These 5 files (file.svg, globe.svg, next.svg, vercel.svg, window.svg) were
created by 'create-next-app' during initial scaffolding and have never been
imported anywhere in the codebase. They were dormant assets shipped in
web/public/ since project init.

Removing them because Biome 2.5.0 enables the new lint/a11y/noSvgWithoutTitle
rule, which fires on these files and breaks the Biome verify step in CI —
blocking every grouped Dependabot non-major run that bundles the biome bump.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rom main (#117)

These two workflows were added on main via #88 and #100 respectively, but
the variant/document-level branch never received them. GitHub Actions
reads workflow files from the PR's base branch — so Dependabot PRs and
human PRs targeting variant currently bypass both the auto-merge and the
Slack notification pipelines.

Bringing them byte-identical from main so the variant line is treated as
a true parallel main branch. After this lands:
- Dependabot grouped PRs against variant get auto-merged when CI is green
- All PRs against variant post to #github-logs with AI-summary

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…updates (#95)

* chore(deps): bump the all-non-major group across 1 directory with 14 updates

Bumps the all-non-major group with 14 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.16` | `2.5.0` |
| [@sanity/client](https://github.com/sanity-io/client) | `7.22.0` | `7.22.1` |
| [next](https://github.com/vercel/next.js) | `16.2.6` | `16.2.9` |
| [next-sanity](https://github.com/sanity-io/next-sanity/tree/HEAD/packages/next-sanity) | `13.0.4` | `13.1.0` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.17` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.0` | `4.3.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.19.19` | `22.19.21` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` |
| [@sanity/code-input](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/code-input) | `7.1.2` | `7.1.3` |
| [@sanity/document-internationalization](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/document-internationalization) | `6.2.1` | `6.2.7` |
| [sanity-plugin-internationalized-array](https://github.com/sanity-io/plugins/tree/HEAD/plugins/sanity-plugin-internationalized-array) | `5.1.3` | `5.1.8` |
| [sanity-plugin-media](https://github.com/sanity-io/sanity-plugin-media) | `4.3.0` | `4.3.1` |



Updates `@biomejs/biome` from 2.4.16 to 2.5.0
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.0/packages/@biomejs/biome)

Updates `@sanity/client` from 7.22.0 to 7.22.1
- [Release notes](https://github.com/sanity-io/client/releases)
- [Changelog](https://github.com/sanity-io/client/blob/main/CHANGELOG.md)
- [Commits](sanity-io/client@v7.22.0...v7.22.1)

Updates `next` from 16.2.6 to 16.2.9
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](vercel/next.js@v16.2.6...v16.2.9)

Updates `next-sanity` from 13.0.4 to 13.1.0
- [Release notes](https://github.com/sanity-io/next-sanity/releases)
- [Changelog](https://github.com/sanity-io/next-sanity/blob/main/packages/next-sanity/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/next-sanity/commits/next-sanity@13.1.0/packages/next-sanity)

Updates `react` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `@types/react` from 19.2.15 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.6 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-postcss)

Updates `@types/node` from 22.19.19 to 22.19.21
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.15 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `tailwindcss` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

Updates `@sanity/code-input` from 7.1.2 to 7.1.3
- [Release notes](https://github.com/sanity-io/plugins/releases)
- [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/code-input/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/plugins/commits/@sanity/code-input@7.1.3/plugins/@sanity/code-input)

Updates `@sanity/document-internationalization` from 6.2.1 to 6.2.7
- [Release notes](https://github.com/sanity-io/plugins/releases)
- [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/document-internationalization/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/plugins/commits/@sanity/document-internationalization@6.2.7/plugins/@sanity/document-internationalization)

Updates `sanity-plugin-internationalized-array` from 5.1.3 to 5.1.8
- [Release notes](https://github.com/sanity-io/plugins/releases)
- [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/sanity-plugin-internationalized-array/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/plugins/commits/sanity-plugin-internationalized-array@5.1.8/plugins/sanity-plugin-internationalized-array)

Updates `sanity-plugin-media` from 4.3.0 to 4.3.1
- [Release notes](https://github.com/sanity-io/sanity-plugin-media/releases)
- [Changelog](https://github.com/sanity-io/sanity-plugin-media/blob/main/CHANGELOG.md)
- [Commits](sanity-io/sanity-plugin-media@v4.3.0...v4.3.1)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-non-major
- dependency-name: "@sanity/client"
  dependency-version: 7.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@sanity/code-input"
  dependency-version: 7.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@sanity/document-internationalization"
  dependency-version: 6.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@types/node"
  dependency-version: 22.19.21
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: next
  dependency-version: 16.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: next-sanity
  dependency-version: 13.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-non-major
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: sanity-plugin-internationalized-array
  dependency-version: 5.1.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: sanity-plugin-media
  dependency-version: 4.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-non-major
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-non-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(format): adapt breakpoints.css to biome 2.5.0's CSS formatter

Biome 2.5.0 reformats multi-line @custom-variant @media wrappers into a
single-line inner @media. Pre-applying the new format here so this
grouped non-major Dependabot bump can pass CI on the new biome version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Damian <hi@damianrosellen.de>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [sanity](https://github.com/sanity-io/sanity/tree/HEAD/packages/sanity) from 5.27.0 to 6.0.0.
- [Release notes](https://github.com/sanity-io/sanity/releases)
- [Changelog](https://github.com/sanity-io/sanity/blob/main/packages/sanity/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/sanity/commits/v6.0.0/packages/sanity)

---
updated-dependencies:
- dependency-name: sanity
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [sanity-plugin-mux-input](https://github.com/sanity-io/sanity-plugin-mux-input) from 2.19.0 to 3.0.0.
- [Release notes](https://github.com/sanity-io/sanity-plugin-mux-input/releases)
- [Changelog](https://github.com/sanity-io/sanity-plugin-mux-input/blob/main/CHANGELOG.md)
- [Commits](sanity-io/sanity-plugin-mux-input@v2.19.0...v3.0.0)

---
updated-dependencies:
- dependency-name: sanity-plugin-mux-input
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@sanity/vision](https://github.com/sanity-io/sanity/tree/HEAD/packages/@sanity/vision) from 5.27.0 to 6.0.0.
- [Release notes](https://github.com/sanity-io/sanity/releases)
- [Changelog](https://github.com/sanity-io/sanity/blob/main/packages/@sanity/vision/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/sanity/commits/v6.0.0/packages/@sanity/vision)

---
updated-dependencies:
- dependency-name: "@sanity/vision"
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@sanity/dashboard](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/dashboard) from 5.0.1 to 6.0.0.
- [Release notes](https://github.com/sanity-io/plugins/releases)
- [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/dashboard/CHANGELOG.md)
- [Commits](https://github.com/sanity-io/plugins/commits/@sanity/dashboard@6.0.0/plugins/@sanity/dashboard)

---
updated-dependencies:
- dependency-name: "@sanity/dashboard"
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.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.

2 participants