refactor(nav, services): one source of truth for what evig offers and where it appears - #389
Merged
Merged
Conversation
… where it appears
Two menus were describing the same organisation and drifting apart. This makes
placement DATA and deletes what nothing read.
## The SSOT break was mine, from an hour earlier
Moving Linux and the open-source registry into Lernen, I listed them by hand
in navigation.tsx — while SERVICE_CONFIGS still fed them to the
Dienstleistungen menu. One item, two menus, two independent lists to keep in
step: exactly the drift the config exists to prevent.
Each service now declares `navGroup: 'services' | 'learn'` ONCE, and both
menus derive from that. A service cannot be in two menus or in none, and
`services-nav.test.ts` asserts precisely that — proved by planting the
duplicate back, which fails with "linuxOpenSource appears in 2 menus
(expected exactly 1)".
## The catalogue
Kept: webDesign (George's call — it is real revenue), linuxOpenSource,
openSourceSolutions. Added `aiAdoption` at /services/ai-robotics: pillar 5
existed only as a /contact link. The page describes how the work goes — look
at the work for a day, pick one task, build it, hand it over so someone on
their side can run and switch it off — and carries the required honesty
boundary: not a silver bullet, not a dependency, not a big firm.
Removed:
hardwareRecycling → off-thesis outright. "Verantwortungsvolle Entsorgung"
is disposal; evig is not a recycler, and the homepage
now says so in as many words.
buildYourComputer → 549 lines of interactive configurator for a service
flagged `available: false`, still selling the previous
org's "Revamped"-Zertifizierung under an English title.
computerRepair → redirects to /it-hilfe. It advertised evig repairing
dataRecovery your machine at CHF 70/h, which contradicts the
marketplace model where the technician sets the price.
Its "booking slug" also booked nothing: the seeded
service_types slugs never matched these hrefs, so the
page always fell through to isBookable: false.
Redirects rather than 404s — the URLs are indexed and the visitor still wants
the same thing. Also deleted /services/[service]/repair, a 135-line orphan
that ignored its own route param and rendered the same page at
/services/<anything>/repair.
## Über uns answers one question now
Nine unsectioned items spanning five unrelated groups — identity, a brand
division, a transactional product (join a pool, pay a share), trust pages,
and two operational explainers including a City of Zürich subsidy evig does
not administer and cannot redeem. Two carried a `new` badge simultaneously.
It was the only multi-item menu without `isSection` eyebrows.
Now five items in two groups: Wer wir sind (Mission, Vision, Projekte) and
Vertrauen (Transparenz, FAQ). `evig ai` and `Abos teilen` moved to Marktplatz,
which gains a third section — from the reader's side these are three ways to
get something, not three parts of an org chart. `So funktioniert’s` and
`Reparaturbonus Zürich` left the nav; both pages remain.
## Config that held German nobody read
src/config/community.ts was 644 lines: INVOLVEMENT_OPTIONS,
PARTNER_INSTITUTIONS and seven full PageContent trees with German titles,
descriptions and CTA labels — a second copy of getInvolved.* in the message
files. Nothing imported any of it except one type. It could have drifted from
the real copy forever without anyone noticing, because nothing read it. Now
22 lines: the type.
## A duplication that cannot be deleted yet, so it is gated instead
WORKSHOP_CATEGORIES carries German name/description per category, and the
messages carry the same strings again. The public page renders the MESSAGE
version; the config version is load-bearing elsewhere — normalizeCategoryId
matches legacy DB rows on the German name, and the admin filter uses it as an
option value. Deleting it needs an admin refactor.
So it can no longer diverge: workshops-config-matches-messages.test.ts fails
if you edit one and not the other. Mutation-tested. The real fix is noted in
the file.
Also removed the last "Vintage" copy: the hardware workshop said
"Vintage-Restauration", and a dead components.communitySpace block advertised
a "Museum für seltene Vintage-Hardware" — rendered by no component at all.
i18n baseline fell 2531 → 2430 source findings, most of it the deleted
community.ts tree. The one new entry is the edited workshop description: a
pre-existing violation whose text changed. Baselined rather than marked
i18n-ok, because it IS UI text in the wrong layer and should stay visible as
debt until the admin refactor removes it.
npm run verify green: lint, umlauts, chrome, docs, compliance, typecheck,
7755 tests (535 suites), production build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
catomean
added a commit
that referenced
this pull request
Aug 26, 2026
The "Dual-Persona Inventory Smoke" turned red on PRs that had nothing to do with it, and the reason is worth stating precisely: **it runs against the LIVE site, not the branch.** So when #389 retired hardware-recycling and deleted the `[service]/repair` route, this check stayed green through that PR and only went red once the deletion DEPLOYED — after which it failed on every subsequent PR, blaming whoever came next. Four dead assertions: /services/hardware-recycling HTTP 404 /services/computer-repair-upgrades/repair HTTP 404 /services/data-recovery-transfer/repair HTTP 404 /services/linux-open-source/repair HTTP 404 The list is now the four service pages that exist, plus one assertion that a retired service URL still REDIRECTS into /it-hilfe — the behaviour #389 deliberately chose over a 404, and which nothing was checking. Gated so it cannot drift again, in either direction: `inventory-routes-match-services.test.ts` asserts every `/services/*` path the smoke expects to be live is an available SERVICE_CONFIGS entry, AND that every available service is smoke-tested. Redirect assertions (those carrying a `urlPattern`) are exempt, because keeping a retired URL alive is the point. It runs in jest, on the branch, before the deploy that would otherwise arm the trap. Mutation-proved: re-adding /services/hardware-recycling fails with that path named. Also folded in here, because it belongs with the title work rather than with workshops (it briefly landed on the wrong branch after a switch with uncommitted changes): - `services/layout.tsx` set `title: { absolute: … }`, which hand-rolled the brand suffix for the hub AND replaced the inherited title.template for the ENTIRE /services subtree. Every service page shipped with no suffix — including the /services/ai-robotics page added hours earlier, which rendered a bare "KI und Robotik im Betrieb". Found by counting "evig" in the rendered <title>, not by reading code; `absolute` is a perfectly valid metadata field, so nothing could flag it. The layout no longer sets a title and the hub declares its own in page.tsx. - `services/web-design-development/layout.tsx` deleted: 21 lines of HARDCODED ENGLISH title and meta description shipping on all eight locales, entirely overridden by its own page, wrapping children in a fragment and doing nothing else. npm run verify green: lint, umlauts, chrome, docs, compliance, typecheck, 7758 tests, production build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot
pushed a commit
that referenced
this pull request
Aug 26, 2026
…one at all (#391) * fix(seo): stop repeating the org name in page titles, and give /abos one at all The layout sets `title.template = '%s | evig'`, so every page title already gets the org name appended. Three separate places added it a second time, each by a different mechanism, and each produced a visible duplicate in the browser tab, in search results and in shared links: / "evig – evig — Intelligenz, für alle bezahlbar … | evig" a page-level prefix AND a message value opening with "evig —" /about "Über uns - evig | evig" generateMetadata appending `${ORG.name}` /knowhow "Knowhow - Guides, Blog, Workshops | evig | evig" the MESSAGE VALUE itself ended in "| evig" I fixed the homepage, then found the other two later by reading rendered titles rather than code. Three instances of one mistake is where the class gets closed, so `page-titles-not-doubled.test.ts` now asserts that no title string in the message files carries the brand. Mutation-proved: putting "| evig" back into knowhow.meta.title fails with the offending key named. The component half (appending ORG.name inside generateMetadata) is not statically checkable from the message files, so each page carries a note saying the template owns the name. openGraph keeps the full form on purpose — a shared card has no template to expand. Two related title defects fixed with it: /abos had NO metadata at all, so a live product page — join a pool, pay a share — fell through to the layout default and wore the homepage's title. In a tab, in search and in a shared link it was indistinguishable from the homepage. /blog was the only page on the site MISSING the suffix, reading a bare "Blog". Its layout and its index page both set a title; the layout's is redundant and now only contributes the description and OG defaults. npm run verify green: lint, umlauts, chrome, docs, compliance, typecheck, 7757 tests (536 suites), production build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(ci): the inventory smoke asserted four routes that #389 deleted The "Dual-Persona Inventory Smoke" turned red on PRs that had nothing to do with it, and the reason is worth stating precisely: **it runs against the LIVE site, not the branch.** So when #389 retired hardware-recycling and deleted the `[service]/repair` route, this check stayed green through that PR and only went red once the deletion DEPLOYED — after which it failed on every subsequent PR, blaming whoever came next. Four dead assertions: /services/hardware-recycling HTTP 404 /services/computer-repair-upgrades/repair HTTP 404 /services/data-recovery-transfer/repair HTTP 404 /services/linux-open-source/repair HTTP 404 The list is now the four service pages that exist, plus one assertion that a retired service URL still REDIRECTS into /it-hilfe — the behaviour #389 deliberately chose over a 404, and which nothing was checking. Gated so it cannot drift again, in either direction: `inventory-routes-match-services.test.ts` asserts every `/services/*` path the smoke expects to be live is an available SERVICE_CONFIGS entry, AND that every available service is smoke-tested. Redirect assertions (those carrying a `urlPattern`) are exempt, because keeping a retired URL alive is the point. It runs in jest, on the branch, before the deploy that would otherwise arm the trap. Mutation-proved: re-adding /services/hardware-recycling fails with that path named. Also folded in here, because it belongs with the title work rather than with workshops (it briefly landed on the wrong branch after a switch with uncommitted changes): - `services/layout.tsx` set `title: { absolute: … }`, which hand-rolled the brand suffix for the hub AND replaced the inherited title.template for the ENTIRE /services subtree. Every service page shipped with no suffix — including the /services/ai-robotics page added hours earlier, which rendered a bare "KI und Robotik im Betrieb". Found by counting "evig" in the rendered <title>, not by reading code; `absolute` is a perfectly valid metadata field, so nothing could flag it. The layout no longer sets a title and the hub declares its own in page.tsx. - `services/web-design-development/layout.tsx` deleted: 21 lines of HARDCODED ENGLISH title and meta description shipping on all eight locales, entirely overridden by its own page, wrapping children in a fragment and doing nothing else. npm run verify green: lint, umlauts, chrome, docs, compliance, typecheck, 7758 tests, production build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two menus were describing the same organisation and drifting apart. This makes placement data and deletes what nothing read.
The SSOT break was mine, from an hour earlier
Moving Linux and the open-source registry into Lernen, I listed them by hand in
navigation.tsx— whileSERVICE_CONFIGSstill fed them to the Dienstleistungen menu. One item, two menus, two independent lists to keep in step: exactly the drift the config exists to prevent.Each service now declares
navGroup: 'services' | 'learn'once, and both menus derive from that. A service cannot be in two menus or in none, andservices-nav.test.tsasserts precisely that — proved by planting the duplicate back, which fails with "linuxOpenSource appears in 2 menus (expected exactly 1)".The catalogue
Kept:
webDesign(your call — real revenue),linuxOpenSource,openSourceSolutions.Added:
aiAdoptionat/services/ai-robotics. Pillar 5 existed only as a/contactlink. The page describes how the work goes — look at the work for a day, pick one task, build it, hand it over so someone on their side can run and switch it off — and carries the required honesty boundary: not a silver bullet, not a dependency, not a big firm.Removed:
hardwareRecyclingbuildYourComputeravailable: false, still selling the previous org's "Revamped"-Zertifizierung under an English title.computerRepair,dataRecovery/it-hilfe. They advertised evig repairing your machine at CHF 70/h, which contradicts the marketplace model where the technician sets the price. The "booking slug" also booked nothing — the seededservice_typesslugs never matched these hrefs, so the page always fell through toisBookable: false.Redirects rather than 404s: the URLs are indexed and the visitor still wants the same thing. Also deleted
/services/[service]/repair, a 135-line orphan that ignored its own route param and rendered the same page at/services/<anything>/repair.Über uns answers one question now
It held nine unsectioned items spanning five unrelated groups — identity, a brand division, a transactional product (join a pool, pay a share), trust pages, and two operational explainers including a City of Zürich subsidy evig does not administer and cannot redeem. Two carried a
newbadge simultaneously. It was the only multi-item menu withoutisSectioneyebrows.Now five items in two groups: Wer wir sind (Mission, Vision, Projekte) and Vertrauen (Transparenz, FAQ).
evig aiandAbos teilenmoved to Marktplatz, which gains a third section — from the reader's side these are three ways to get something, not three parts of an org chart.So funktioniert'sandReparaturbonus Zürichleft the nav; both pages remain.Config that held German nobody read
src/config/community.tswas 644 lines:INVOLVEMENT_OPTIONS,PARTNER_INSTITUTIONSand seven fullPageContenttrees with German titles, descriptions and CTA labels — a second copy ofgetInvolved.*in the message files.Nothing imported any of it except one type. It could have drifted from the real copy forever without anyone noticing, because nothing read it. Now 22 lines: the type.
A duplication that can't be deleted yet — so it's gated instead
WORKSHOP_CATEGORIEScarries a Germanname/descriptionper category, and the messages carry the same strings again. The public page renders the message version; the config version is load-bearing elsewhere —normalizeCategoryIdmatches legacy DB rows on the German name, and the admin filter uses it as an option value. Deleting it needs an admin refactor.So it can no longer diverge:
workshops-config-matches-messages.test.tsfails if you edit one and not the other. Mutation-tested. The real fix is noted in the file.Also removed the last "Vintage" copy: the hardware workshop said "Vintage-Restauration", and a dead
components.communitySpaceblock advertised a "Museum für seltene Vintage-Hardware" — rendered by no component at all.Baseline
i18n source findings fell 2531 → 2430, most of it the deleted
community.tstree. The one new entry is the edited workshop description — a pre-existing violation whose text changed. Baselined rather than markedi18n-ok, because it is UI text in the wrong layer and should stay visible as debt until the admin refactor removes it.Verification
npm run verifygreen: lint, umlauts, chrome, docs, compliance, typecheck, 7755 tests (535 suites), production build.🤖 Generated with Claude Code