Skip to content

refactor(nav, services): one source of truth for what evig offers and where it appears - #389

Merged
github-actions[bot] merged 2 commits into
mainfrom
refactor/nav-groups
Aug 26, 2026
Merged

refactor(nav, services): one source of truth for what evig offers and where it appears#389
github-actions[bot] merged 2 commits into
mainfrom
refactor/nav-groups

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

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 (your call — 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:

service why
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, dataRecovery Redirect to /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 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

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 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 can't be deleted yet — so it's gated instead

WORKSHOP_CATEGORIES carries a 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.

Baseline

i18n source findings fell 2531 → 2430, 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.

Verification

npm run verify green: lint, umlauts, chrome, docs, compliance, typecheck, 7755 tests (535 suites), production build.

🤖 Generated with Claude Code

catomean and others added 2 commits August 26, 2026 11:58
… 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>
@github-actions
github-actions Bot merged commit 5d526f3 into main Aug 26, 2026
9 checks passed
@github-actions
github-actions Bot deleted the refactor/nav-groups branch August 26, 2026 10:56
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant