Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
46bdf13
WIP variant/document-level — Step 1+2: plugin + all 9 schemas
damianrosellen1 May 28, 2026
90c7264
variant: Step 3+4 — structure items, slug uniqueness, presentation
damianrosellen1 May 28, 2026
0c5799e
variant: Step 5–9 — full web migration to document-level translation
damianrosellen1 May 28, 2026
b86135a
feat(MediaImage): enable client-side rendering by adding "use client"…
MCLWallet May 28, 2026
9afe8b7
refactor: remove fallback to NEXT_PUBLIC_SANITY_PROJECT_ID in dataset…
MCLWallet May 28, 2026
3a69f59
refactor: streamline project ID retrieval in dataset resolution
MCLWallet May 28, 2026
e8baa55
refactor(MediaImage): remove unnecessary line breaks for improved rea…
MCLWallet May 28, 2026
c6b4efa
feat(carousel): implement module.carousel with embla (loop, dots, thu…
damianrosellen1 May 28, 2026
fae67db
fix(layout): defer img-loaded class to next animation frame to avoid …
damianrosellen1 May 28, 2026
c1f45dc
fix(sanity): use NEXT_PUBLIC_* only for sync project id and dataset t…
damianrosellen1 May 28, 2026
ff5e628
fix(media): manage img-loaded class via React state to fix hydration …
damianrosellen1 May 28, 2026
1d4ddd7
feat(sanity): enhance siteNav queries for locale support
MCLWallet May 28, 2026
5227eae
Merge branch 'variant/document-level' of https://github.com/backendfo…
MCLWallet May 28, 2026
d3f1704
refactor(web): expose SANITY_STUDIO_* to client via next.config env map
damianrosellen1 May 28, 2026
b56489f
fix(media): reveal images that failed to load instead of leaving them…
damianrosellen1 May 28, 2026
9b8a32a
feat(sanity): wire Presentation overlay onto rendered modules via dat…
damianrosellen1 May 28, 2026
84c1d21
chore(layout): log SanityLive connection errors to console.error
damianrosellen1 May 28, 2026
f9d502a
feat(layout): integrate DocumentBootScript for theme management
MCLWallet May 28, 2026
837e3a4
feat(linkResolver): enhance link handling in RichTextMedia and Module…
damianrosellen1 May 28, 2026
c4720bc
feat(modules): add ModulesRendererClient for client-side module reord…
damianrosellen1 May 28, 2026
02b1016
feat(modules): add ModulesRendererClient for client-side module reord…
damianrosellen1 May 28, 2026
3efd441
fix(sanity): move SanityLive onError into a "use client" module
damianrosellen1 May 28, 2026
65452f8
feat(sanity): scope variant data-sanity to document-level 'modules' path
damianrosellen1 May 28, 2026
aced9ad
feat(header): enhance language switcher functionality in mobile menu
MCLWallet May 28, 2026
2653d81
refactor(header): improve formatting of LanguageSwitch component in H…
MCLWallet May 28, 2026
6ef5ce0
docs(readme): polish for public release (document-level variant)
damianrosellen1 May 28, 2026
9079800
feat(navigation): add curated nav.themeToggle module
damianrosellen1 May 29, 2026
b5d6319
refactor(styles): rename variables/typography.css to fontsizes.css an…
damianrosellen1 May 29, 2026
12a4eda
fix(i18n): default `language` to "en" on new translatable documents
damianrosellen1 May 29, 2026
7e3a236
feat(cookies): port vanilla-cookieconsent banner from bef-website-2026
damianrosellen1 May 29, 2026
ff34d24
docs(readme): document cookie banner + drop unused letter-spacing decls
damianrosellen1 May 29, 2026
8309dd2
chore: rename from "boilerplate" to "starter"
damianrosellen1 May 29, 2026
39292c0
fix(typography): move RichTextMedia spacing to global CSS
damianrosellen1 Jun 3, 2026
88902dc
refactor(links): resolve nav link labels in-app via resolveLinkLabel
damianrosellen1 Jun 3, 2026
6d464d1
feat: port main updates to variant/document-level
damianrosellen1 Jun 3, 2026
3c8c9fa
Merge pull request #72 from backendforth/port/main-to-variant
damianrosellen1 Jun 3, 2026
95106d1
docs(agents): add AI/agent guardrails (ported from main with variant-…
damianrosellen1 Jun 4, 2026
4618c76
feat(tooling): port gen:module + check:wiring + best-practices guardr…
damianrosellen1 Jun 4, 2026
26b1734
style(scaffold-module): apply biome auto-format to optional-patch helper
damianrosellen1 Jun 4, 2026
43b35af
Merge pull request #73 from backendforth/feat/agent-guardrails-variant
damianrosellen1 Jun 4, 2026
9d262c9
chore(deps): mirror dependabot.yml from main (config-only)
damianrosellen1 Jun 15, 2026
99acb24
Merge pull request #89 from backendforth/chore/dependabot-mirror-from…
damianrosellen1 Jun 15, 2026
3dec3ac
chore: remove unused create-next-app boilerplate SVGs (#116)
damianrosellen1 Jun 15, 2026
771d6c0
chore(ci): mirror dependabot-auto-merge + pr-slack-notify workflows f…
damianrosellen1 Jun 15, 2026
538225b
chore(deps): bump the all-non-major group across 1 directory with 14 …
dependabot[bot] Jun 15, 2026
2e7a729
chore(deps): bump sanity from 5.27.0 to 6.0.0 (#96)
dependabot[bot] Jun 15, 2026
297899c
chore(deps): bump sanity-plugin-mux-input from 2.19.0 to 3.0.0 (#97)
dependabot[bot] Jun 15, 2026
c1bc5d2
chore(deps): bump @sanity/vision from 5.27.0 to 6.0.0 (#98)
dependabot[bot] Jun 15, 2026
23eed84
chore(deps): bump @sanity/dashboard from 5.0.1 to 6.0.0 (#99)
dependabot[bot] Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .cursor/rules/core.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
description: Project-wide guardrails. Always applied.
alwaysApply: true
---

Canonical guardrails: `/AGENTS.md`. Read it before writing code in this repo.

Top-level rules (do not duplicate — see AGENTS.md for the full context):

- Reuse before invention; grep `studio/schemas/objects/modules/`, `web/src/components/modules/`, `web/sanity/queries/snippets/` first.
- Content blocks are **modules** — paired Sanity object + React component, wired in 8 places. Skipping any one is a bug.
- Locale-aware at render time, never inside GROQ. Always use `pickLocalizedString` / `parseLocalizedText` from `web/sanity/utils/sanityLocalizedText.ts`.
- After schema edits: `pnpm studio:generate` → `pnpm typecheck` → `pnpm format`. Never edit `studio/sanity.types.gen.ts` or `studio/schema.json`.
- `web/sanity/types/*` are hand-maintained; keep aligned with field changes.
36 changes: 36 additions & 0 deletions .cursor/rules/modules.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
description: Module pattern — 8-step wiring. Applies when editing modules anywhere.
globs:
- studio/schemas/objects/modules/**
- web/src/components/modules/**
- web/sanity/queries/components/modules/**
- web/sanity/types/modules/**
---

A **module** is a paired Sanity object (`module.<id>`) plus a React component (`Module<Name>.tsx`). Adding, renaming, or removing one is atomic across **8 files**:

1. `studio/schemas/objects/modules/module<Name>.ts` — `defineType`, `name: "module.<id>"`, fields, `preview`, optional `icon`.
2. `studio/schemas/index.ts` — import + add to `schemaTypes`.
3. `studio/schemas/objects/editors/richTextMedia.ts` — append `{ type: "module.<id>" }` to `of`.
4. `studio/schemas/fields/modulesArrayField.ts` — append `{ type: "module.<id>" }` to `moduleTypes`.
5. `web/src/components/modules/Module<Name>.tsx` — accepts `{ data, locale, siteLocale }`, sets `data-sanity` attrs.
6. `web/src/components/modules/index.ts` — re-export from barrel and register in `ModulesRenderer.tsx`.
7. `web/sanity/queries/components/modules/<name>.ts` — GROQ projection, full i18n arrays, re-export from `index.ts`.
8. `web/sanity/types/modules/<name>.ts` — hand-maintained TS shape, re-export from `index.ts`.

After steps 1–4 land, run `pnpm studio:generate` and commit the gen artifacts.

DO:

- Ship all 8 in a single commit.
- Reuse `media.image`, `media.video`, `media.videoLoop` from `studio/schemas/objects/media/`.
- Project i18n fields as full `{ _key, _type, language, value }` arrays.
- Read i18n via `pickLocalizedString` / `parseLocalizedText`.

DON'T:

- Land schema without component (or vice versa).
- Add `{ type: "module.<id>" }` to only one of `richTextMedia.ts` / `modulesArrayField.ts`.
- Index i18n arrays directly (`title[0].value`).
- `coalesce(field[language==$locale].value, ...)` in GROQ.
- Hand-edit `studio/sanity.types.gen.ts` or `studio/schema.json`.
36 changes: 36 additions & 0 deletions .cursor/rules/schemas.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
description: Sanity schema conventions and typegen workflow.
globs:
- studio/schemas/**
- studio/config/**
---

Sanity Studio v5. Schemas live under `studio/schemas/`. Indent 2 spaces, double quotes.

Hard rules:

- A new type is invisible until exported and added to `schemaTypes` in `studio/schemas/index.ts`.
- New document types also need a structure item under `studio/config/structure/items/` and registration in `studio/config/structure/index.ts`. The sidebar does NOT auto-populate from schemas.
- Routable types must be wired into Presentation: `studio/config/presentation/conventions.ts` (`SLUG_BASED_DOCUMENT_TYPES`, `SITE_ROOT_DOCUMENT_TYPES`, `DOCUMENT_TYPES_WITHOUT_WEB_PREVIEW`) and `resolve.ts` / `locationsResolver.ts`.
- Internally linkable types go into `studio/schemas/constants/references.ts` (`PAGE_REFERENCES`).
- Translatable fields use `internationalizedArrayString` / `internationalizedArrayRichText` / `internationalizedArrayRichTextMedia` — never plain `string` for translatable content.
- Slug fields use `validateSlug` from `studio/utils/validateSlug.ts`.
- Reuse `media.image`, `media.video`, `media.videoLoop` from `studio/schemas/objects/media/`.

After every schema edit:

```
pnpm studio:generate
git add studio/schema.json studio/sanity.types.gen.ts
pnpm typecheck
```

CI rejects any uncommitted gen diff.

NEVER edit:

- `studio/sanity.types.gen.ts`
- `studio/schema.json`
- Anything under `studio/.sanity/`

Languages come from the `siteLanguageSettings` singleton — add new languages there, not in source. `web/src/i18n/fallbackSiteLocales.ts` is the offline fallback only.
33 changes: 33 additions & 0 deletions .cursor/rules/web.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
description: Next.js web app conventions — routing, i18n, components.
globs:
- web/**
---

Next.js 16 App Router. RSC by default; `"use client"` only when needed (state, effects, browser APIs). Tailwind v4. Biome: tabs, double quotes. Path alias `@/*` → `web/`.

Hard rules:

- Thread `{ locale, siteLocale }` through every render path that touches Sanity content.
- Resolve translatable fields via `pickLocalizedString` / `parseLocalizedText` from `web/sanity/utils/sanityLocalizedText.ts`. Never index i18n arrays.
- No locale filtering inside GROQ — projections return full `{ _key, _type, language, value }` arrays.
- Content blocks are **modules** (see `.cursor/rules/modules.mdc`). No ad-hoc `<section>` in route files.
- `web/sanity/types/*` are hand-maintained. Update them in the same commit as the GROQ projection.
- Set `data-sanity` attrs on Sanity-rendered roots (copy from `ModuleText.tsx` / `MediaImage.tsx`) — Visual Editing depends on it.
- Never read locale from `useRouter()`, `usePathname()`, `window.location`, or cookies inside a module — breaks SSR + Presentation iframes.
- No hardcoded locale codes outside `web/src/i18n/`.

URL/locale helpers live in `web/src/i18n/` (`paths.ts`, `siteLocalePathUtils.ts`, `site-locales.ts`, `proxyLocaleFetch.ts`). Use them; do not concatenate locale prefixes by hand.

After web changes:

```
pnpm typecheck
pnpm format
```

NEVER edit:

- `web/.next/`
- Any `*.gen.*` files
- Imports from `studio/...` (use `packages/*` for shared code).
57 changes: 57 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# GitHub Copilot — repository instructions

Full guardrails live in `AGENTS.md` at the repo root. This file is a condensed mirror for inline suggestions.

## Repo at a glance

- pnpm monorepo: `web` (Next.js 16 App Router), `studio` (Sanity v5), shared `packages/*`.
- Editorial content is rendered via the **module pattern**: every content block is a paired `module.<name>` Sanity object + `Module<Name>.tsx` React component, wired in 8 places.
- Multilingual content uses `@sanity/internationalized-array`. Locale is resolved at **render time** via `web/sanity/utils/sanityLocalizedText.ts`, never inside GROQ.

## Top rules

1. Reuse before invention — check `studio/schemas/objects/modules/`, `web/src/components/modules/`, `web/sanity/queries/snippets/`, `web/sanity/utils/` first.
2. New content block → it is a **module**. Touch all 8 wiring points or revert:
- `studio/schemas/objects/modules/module<Name>.ts`
- `studio/schemas/index.ts`
- `studio/schemas/objects/editors/richTextMedia.ts`
- `studio/schemas/fields/modulesArrayField.ts`
- `web/src/components/modules/Module<Name>.tsx`
- `web/src/components/modules/index.ts` (+ register in `ModulesRenderer.tsx`)
- `web/sanity/queries/components/modules/<name>.ts` (+ barrel)
- `web/sanity/types/modules/<name>.ts` (+ barrel)
3. After every schema edit: `pnpm studio:generate`, then `pnpm typecheck`, then `pnpm format`.
4. Never edit `studio/sanity.types.gen.ts` or `studio/schema.json` — generated.
5. `web/sanity/types/*` are hand-maintained — update them when schema fields change.
6. Locale-aware always: use `pickLocalizedString(field, locale, siteLocale)` or `parseLocalizedText({ value, locale, siteLocale, as })`. Never `field[0].value` or `field.find(t => t.language === locale)`. Never `coalesce(field[language==$locale].value, ...)` in GROQ.
7. Add a new language only via the `siteLanguageSettings` Studio singleton (offline fallback: `web/src/i18n/fallbackSiteLocales.ts`). No schema, query, or component change.
8. No hardcoded locale codes outside `web/src/i18n/`.
9. No imports from `studio/` inside `web/` (or vice versa). Share via `packages/*`.

## Naming

- Sanity module name `module.<id>` ↔ React component `Module<Name>.tsx`.
- Path alias `@/*` resolves to `web/` only.
- Biome: tabs (web + root), 2 spaces (studio), double quotes throughout.

## Decision tree — where things live

- Authored content block? → module.
- Layout primitive (header, footer, container)? → `web/src/components/{navigation,theme,…}/`.
- Sanity-only helper? → `studio/utils/` or `studio/config/`.
- Shared between web + studio? → a package in `packages/*`.
- Locale string / URL helper? → `web/src/i18n/`.
- Reusable GROQ fragment? → `web/sanity/queries/snippets/`.

## Branches

The repo has two long-lived branches with non-trivial differences:

- `main` — document types `page`, `project`, `projectCategory`, `work`; four web module renderers all under `web/src/components/modules/` with an `index.ts` barrel; `module.contentRefs` schema supports project filtering.
- `variant/document-level` — only `page` (no projects/work); all four module schemas exist but only `ModuleMedia` and `ModuleText` have local renderers — `ModuleCarousel` lives in `web/src/components/carousel/`, `ModuleContentRefs` has a dev-only placeholder. No `web/src/components/modules/index.ts` barrel. `module.contentRefs` is simplified to `PAGE_REFERENCES` with an `allowMultiple` toggle. `page` has a `language` string field set by the i18n plugin — **never edit manually**.

Check `git rev-parse --abbrev-ref HEAD` and the actual file layout on the branch before adding modules.

## Definition of done

`pnpm typecheck` passes, `pnpm format` clean, gen artifacts committed if schema changed, all 8 module wiring points touched (when applicable), no `--no-verify`.
60 changes: 52 additions & 8 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,64 @@
version: 2

# Two parallel pipelines because `main` and `variant/document-level` are
# treated as independently-shipped lines of the starter, not as
# trunk + downstream. Dependabot reads this file from the default branch
# only (`main`); the `target-branch` field is what fans updates out.
#
# Both entries share the same shape:
# - monthly schedule (first Monday 06:00 CET)
# - one catch-all `all-non-major` group bundles every minor + patch
# across the workspace into a single PR per run; majors stay
# individual for manual review (e.g. Next 16→17, React 19→20)
# - `@types/node` major bumps ignored (pinned to Node 22 LTS)
#
# Keep the two entries in lockstep — when editing either, mirror the
# change to the other.
#
# Security advisories are NOT schedule-gated and arrive immediately on
# both branches regardless of the monthly cadence.

updates:
# ── main ───────────────────────────────────────────────────────────────────
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
interval: monthly
day: monday
open-pull-requests-limit: 10
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
groups:
sanity-ecosystem:
all-non-major:
patterns:
- "sanity"
- "@sanity/*"
- "sanity-plugin-*"
- "*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
labels:
- "dependencies"

# ── variant/document-level ─────────────────────────────────────────────────
- package-ecosystem: npm
directory: "/"
target-branch: "variant/document-level"
schedule:
interval: monthly
day: monday
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
groups:
all-non-major:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
# @types/node must track the runtime (Node 22 LTS), not the latest major.
# Allow minor/patch within v22; block major bumps (e.g. 25.x).
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
labels:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ jobs:
- name: Ensure formatter produced no changes
run: git diff --exit-code
- run: pnpm run typecheck
# Wiring drift between schema, component, query, and type files is a
# silent bug class — `pnpm gen:module` creates them atomically, but
# hand-edits routinely miss a file. This gate catches partial wirings
# before they reach main.
- run: pnpm run check:wiring

schema-typegen:
name: schema typegen
Expand Down Expand Up @@ -84,7 +89,7 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: pnpm --filter web run build
env:
# Builds without a project must still succeed (boilerplate). Real
# Builds without a project must still succeed (starter). Real
# deploys provide SANITY_STUDIO_PROJECT_ID via the host's env.
NEXT_TELEMETRY_DISABLED: "1"

Expand Down
43 changes: 43 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Dependabot auto-merge

# Enables GitHub auto-merge on Dependabot PRs for patch + minor bumps
# (including grouped PRs). GitHub then waits for ALL required checks to
# pass before squash-merging. Auto-merge ≠ merge-regardless — failing CI
# leaves the PR sitting open until you act on it.
#
# Major bumps fall through and remain manual: fetch-metadata reports the
# highest update-type across a group, and our dependabot.yml group filter
# (`update-types: [minor, patch]`) excludes majors, so single-major PRs
# return `version-update:semver-major` and skip the merge step.

on:
pull_request:
# Both branches are treated as parallel main lines; Dependabot opens
# PRs against both (see dependabot.yml).
branches:
- main
- variant/document-level

permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Fetch dependabot metadata
id: meta
uses: dependabot/fetch-metadata@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Enable auto-merge for patch + minor
if: |
steps.meta.outputs.update-type == 'version-update:semver-patch' ||
steps.meta.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 changes: 13 additions & 0 deletions .github/workflows/pr-slack-notify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: PR Slack notify

on:
pull_request:
types: [opened, ready_for_review, closed]
branches:
- main
- variant/document-level

jobs:
notify:
uses: backendforth/.github/.github/workflows/pr-slack-summary.yml@main
secrets: inherit
2 changes: 1 addition & 1 deletion .github/workflows/strip-readmes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: remove README files (boilerplate cleanup)"
commit_message: "chore: remove README files (starter cleanup)"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ out/

# Editors / agents
.claude
.cursor
.cursor/*
!.cursor/rules/

# Coverage / test artefacts
coverage/
Loading