From d033ae30ac1b418b4ad4b5ee7699e95de8757d80 Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:23:44 +0200 Subject: [PATCH 1/9] =?UTF-8?q?docs(claude):=20rewrite=20=C2=A78=20to=20Gi?= =?UTF-8?q?tFlow=20(#40)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(claude): rewrite §8 to GitFlow (develop = integration, main = releases) Also add a no-commit-to-branch pre-commit guard mirroring the server-side main-develop ruleset (commit-stage only; keeps the review-gate pre-push symlink). * ci: run CI on PRs to develop (GitFlow) — unblocks develop-based PRs --- .github/workflows/ci.yml | 4 ++-- .pre-commit-config.yaml | 20 ++++++++++++++++++++ CLAUDE.md | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a822906..792b544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,9 +24,9 @@ name: CI on: push: - branches: [main] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] concurrency: group: ci-${{ github.ref }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9582cb0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +# Sofra — pre-commit configuration +# +# Primary purpose: the **no-commit-to-branch** guard. Since 2026-07-10 both +# `main` and `develop` are protected server-side by the no-bypass `main-develop` +# GitHub Ruleset (GitFlow: `develop` = integration, `main` = releases only) — +# this hook mirrors that locally so a direct commit to a protected branch is +# blocked before it ever reaches a push. Branch + PR always. +# +# Install (commit-stage only — deliberately NOT `--hook-type pre-push`, which +# would clobber the review-gate pre-push symlink; see workspace memory +# review-gate-precommit-clobber): +# pre-commit install +# Run manually: pre-commit run --all-files +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: no-commit-to-branch + # `master` included defensively against an accidental rename/fork. + args: ["--branch", "main", "--branch", "develop", "--branch", "master"] diff --git a/CLAUDE.md b/CLAUDE.md index 3488681..1812430 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ Output before writing code: (1) which `require*()` guard covers each new surface ## §8 — Git workflow -- Branch off `main`, PR to `main` (`feature/` `fix/` `chore/` `docs/`). Commits: `type(scope): description`. +- **GitFlow:** `develop` = default + integration branch — branch off it and open every `feature/`·`fix/`·`chore/`·`docs/` PR **to `develop`**; merge only when CI is green + comments resolved. **`main` = releases only**, via a `develop`→`main` release PR. Both branches are protected by the no-bypass `main-develop` ruleset (blocks direct push; open a PR). Commits: `type(scope): description`. - **Merge ≠ deploy.** A merge builds the image; rollout is manual on the staging box: `docker compose -f docker-compose.prod.yml pull sofra && up -d sofra` (in `/opt/rumi/deploy`, via `deploy/.ssh/staging.sh`). **Schema changes: run the migrate one-off BEFORE rolling the app.** - PR body uses `.github/pull_request_template.md` — including the NFR triage section (DEV-PHASES-PLAN P1). From 2876cab4d09e01faa14c28d7553c383e343a39e7 Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:04:16 +0200 Subject: [PATCH 2/9] =?UTF-8?q?ci+docs:=20run=20CI=20on=20PRs=20to=20devel?= =?UTF-8?q?op=20(GitFlow)=20+=20branch=20guard=20+=20=C2=A78=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(claude): rewrite §8 to GitFlow (develop = integration, main = releases) Also add a no-commit-to-branch pre-commit guard mirroring the server-side main-develop ruleset (commit-stage only; keeps the review-gate pre-push symlink). * ci: run CI on PRs to develop (GitFlow) — unblocks develop-based PRs From bb2e4d54cd4f15a46d2692a8e0419adf3f017edc Mon Sep 17 00:00:00 2001 From: mahmutKaya <33642821+mahmutkaya@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:04:15 +0200 Subject: [PATCH 3/9] =?UTF-8?q?feat(aeo):=20content=20engine=20=E2=80=94?= =?UTF-8?q?=20changelog,=20RUMI=20case=20study,=20GloriaFood=20comparison?= =?UTF-8?q?=20(sofra=20#6)=20(#42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AEO plan §2 slice: three localized public marketing pages under [locale], in all six locales, with per-page hreflang/canonical metadata + JSON-LD. - /changelog — dated what's-new entries (data-driven via components/changelog-data.ts + changelog.* messages; WebPage JSON-LD with dateModified = newest entry) - /case/rumi — RUMI Geneva case study, verifiable facts only (live since 2026-06-29, 10 guest languages, EU infra); the quantitative stats section is deliberately omitted (owner-owed), with an honest "where are the numbers?" note instead. Article JSON-LD. - /compare/gloriafood — honest, table-driven Sofra-vs-GloriaFood page with a visible "last checked" date; every GloriaFood claim verified 2026-07-10 against their public pages (URLs in components/compare-gloriafood-data.ts and rendered as a visible Sources section). Conservative wording for anything unverified (website/app add-on prices described qualitatively only). Cross-cutting: - lib/seo.ts: shared SITE_URL + pageAlternates/marketingPageMetadata (layout refactored to use it); unit-tested and added to the coverage floor scope - components/JsonLdScript.tsx: shared hardened JSON-LD renderer (JsonLd.tsx refactored onto it) - Header/Footer anchors switched to locale-aware /#anchor links so the nav works from subpages; footer links the three new pages - sitemap.xml now lists all 4 routes x 6 locales; llms.txt mentions the new pages - messages: +71 keys per locale, parity green across en/fr/de/nl/tr/ar Validation: typecheck, eslint, next build (all routes SSG x6 locales), vitest 89 passed + coverage floor, i18n parity, check-single-file --all, runtime smoke (200 + JSON-LD + hreflang + RTL verified on next start). Co-authored-by: Claude Fable 5 --- app/[locale]/case/rumi/page.tsx | 149 ++++++++++++++++ app/[locale]/changelog/page.tsx | 84 +++++++++ app/[locale]/compare/gloriafood/page.tsx | 156 +++++++++++++++++ app/[locale]/layout.tsx | 12 +- app/sitemap.ts | 20 ++- components/CompareTable.tsx | 45 +++++ components/Footer.tsx | 38 ++-- components/Header.tsx | 29 +-- components/JsonLd.tsx | 20 +-- components/JsonLdScript.tsx | 21 +++ components/changelog-data.ts | 13 ++ components/compare-gloriafood-data.ts | 34 ++++ lib/seo.ts | 43 +++++ messages/ar.json | 214 ++++++++++++++++++++++- messages/de.json | 214 ++++++++++++++++++++++- messages/en.json | 214 ++++++++++++++++++++++- messages/fr.json | 214 ++++++++++++++++++++++- messages/nl.json | 214 ++++++++++++++++++++++- messages/tr.json | 214 ++++++++++++++++++++++- public/llms.txt | 8 + tests/unit/seo.test.ts | 49 ++++++ vitest.config.ts | 1 + 22 files changed, 1943 insertions(+), 63 deletions(-) create mode 100644 app/[locale]/case/rumi/page.tsx create mode 100644 app/[locale]/changelog/page.tsx create mode 100644 app/[locale]/compare/gloriafood/page.tsx create mode 100644 components/CompareTable.tsx create mode 100644 components/JsonLdScript.tsx create mode 100644 components/changelog-data.ts create mode 100644 components/compare-gloriafood-data.ts create mode 100644 lib/seo.ts create mode 100644 tests/unit/seo.test.ts diff --git a/app/[locale]/case/rumi/page.tsx b/app/[locale]/case/rumi/page.tsx new file mode 100644 index 0000000..f97f930 --- /dev/null +++ b/app/[locale]/case/rumi/page.tsx @@ -0,0 +1,149 @@ +import type { Metadata } from "next"; +import { setRequestLocale, getTranslations } from "next-intl/server"; +import { Link } from "@/i18n/navigation"; +import Header from "@/components/Header"; +import Footer from "@/components/Footer"; +import SectionLabel from "@/components/SectionLabel"; +import JsonLdScript from "@/components/JsonLdScript"; +import { SITE_URL, marketingPageMetadata } from "@/lib/seo"; + +// RUMI case study (AEO plan §2 priority 1). Verifiable facts only — the +// quantitative stats section is deliberately absent until the owner can +// publish numbers we can stand behind (no placeholders, no invented metrics). + +const RUMI_URL = "https://www.rumirestaurant.ch"; +const LIVE_SINCE = "2026-06-29"; +const PUBLISHED = "2026-07-10"; +const RUN_KEYS = ["ordering", "boards", "reservations", "loyalty", "printing"] as const; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "caseStudy" }); + return marketingPageMetadata({ + locale, + path: "/case/rumi", + title: t("meta.title"), + description: t("meta.description"), + }); +} + +export default async function RumiCaseStudyPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + setRequestLocale(locale); + const t = await getTranslations({ locale, namespace: "caseStudy" }); + const liveSince = new Intl.DateTimeFormat(locale, { + dateStyle: "long", + timeZone: "UTC", + }).format(new Date(LIVE_SINCE)); + + const article = { + "@context": "https://schema.org", + "@type": "Article", + headline: t("meta.title"), + description: t("meta.description"), + datePublished: PUBLISHED, + dateModified: PUBLISHED, + inLanguage: locale, + mainEntityOfPage: `${SITE_URL}/${locale}/case/rumi`, + author: { "@type": "Organization", name: "Sofra", url: SITE_URL }, + publisher: { "@type": "Organization", name: "Sofra", url: SITE_URL }, + about: { + "@type": "Restaurant", + name: "RUMI Restaurant", + url: RUMI_URL, + address: { "@type": "PostalAddress", addressLocality: "Geneva", addressCountry: "CH" }, + }, + }; + + const facts: Array<{ key: string; value: string }> = [ + { key: "location", value: t("facts.location.value") }, + { key: "since", value: liveSince }, + { key: "languages", value: t("facts.languages.value") }, + { key: "hosting", value: t("facts.hosting.value") }, + ]; + + return ( + <> + +
+
+
+ {t("label")} +

+ {t("title")} +

+

{t("subtitle")}

+ +
+ {facts.map(({ key, value }) => ( +
+
+ {t(`facts.${key}.label`)} +
+
{value}
+
+ ))} +
+ +
+

{t("runs.title")}

+

{t("runs.intro")}

+
    + {RUN_KEYS.map((key) => ( +
  • +

    + {t(`runs.items.${key}.title`)} +

    +

    + {t(`runs.items.${key}.text`)} +

    +
  • + ))} +
+
+ +
+

{t("languagesSection.title")}

+

+ {t("languagesSection.body")} +

+
+ +
+

{t("infra.title")}

+

+ {t("infra.body", { date: liveSince })} +

+
+ +
+

{t("numbers.title")}

+

{t("numbers.body")}

+
+ +
+

{t("ctaTitle")}

+

{t("ctaBody")}

+
+ + {t("ctaButton")} + + + {t("visit")} ↗ + +
+
+
+
+