Skip to content

Replace Hugo site with static pages - #1

Merged
carstenb merged 12 commits into
masterfrom
redesign-2026
Sep 9, 2026
Merged

carstenb merged 12 commits into
masterfrom
redesign-2026

Conversation

@carstenb

@carstenb carstenb commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Rebuilds the site as four hand-written static HTML pages from the design handoff, and replaces the Hugo setup entirely.

Why

The site has been offline for years for two independent reasons: the repo is private, and GitHub Pages was never re-enabled (has_pages: false). On top of that baseurl pointed at https://example.com/, so CSS, JS and favicons on the deployed site resolved to example.com — it was broken before it went dark.

Hugo was doing nothing here: the theme was 100% overridden and never executed, the homepage copy lived in the template rather than in content, and two used APIs (Page.Hugo, Page.URL) were removed in Hugo 0.90.

What is here

  • index.html, cv/, impressum/, 404.html — copy taken verbatim from the handoff
  • One stylesheet, page-specific rules scoped by body class
  • Fonts self-hosted (4 woff2, latin) with their OFL licences — zero external requests, which is what makes the rewritten privacy text accurate
  • Accessibility built in: landmarks, skip link, visible focus, 24×24 targets, reduced-motion and forced-colors handling
  • Root-absolute paths throughout — required, because Pages serves 404.html from arbitrary paths
  • CI gates deployment: deploy-pages runs only after validate passes

Not done yet, on purpose

  • The repo is still private and Pages is still off, so deploy-pages is skipped via vars.PAGES_ENABLED
  • The imprint's privacy and hosting text needs Carsten's sign-off before it goes public

The previous Hugo site is preserved at the tag old-site-2021.

🤖 Generated with Claude Code

carstenb and others added 12 commits September 9, 2026 14:34
Rebuild the site as four hand-written HTML pages from the design handoff.
The Hugo setup is gone: its theme was 100% overridden and never executed,
the homepage copy lived in the template rather than in content, and two
used APIs (Page.Hugo, Page.URL) were removed in Hugo 0.90.

- index, cv/, impressum/, 404 — copy taken verbatim from the handoff
- assets/site.css merges foundation.css and the four per-page style
  blocks; page-specific rules are scoped by body class
- fonts self-hosted (4 woff2, latin subset) with their OFL licences, so
  nothing is fetched from Google — the privacy text says so and now
  matches reality
- <image-slot> scaffolding replaced with plain <img>; the grid cell stays
  the hover target
- accessibility: nav/main/footer landmarks, skip link, visible focus,
  24x24 targets, reduced-motion and forced-colors handling
- asset and link paths are root-absolute — required because Pages serves
  404.html from arbitrary paths
- imprint: developer .todo removed, privacy text rewritten for
  self-hosted fonts, hosting section added
- LICENSE.md dropped: it was the Hugo theme author's MIT licence and no
  longer applies to this content

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Images:
- portrait was 311 KB for a 400x600 source; recompressed and served as
  200/264/340/400w via srcset+sizes matching the three portrait
  breakpoints, with fetchpriority=high since it is above the fold
- logo-cint 172 KB -> 20 KB, logo-smartrecruiters 92 KB -> 40 KB by
  downscaling to 400w (they render at ~141px at most)
- width/height on every image, loading=lazy on the five company logos

Metadata:
- robots.txt and sitemap.xml (only / and /cv/ — the imprint is noindex by
  choice, 404 is never a destination, no unmaintained lastmod)
- ProfilePage/Person JSON-LD with a stable @id and sameAs
- google-site-verification carried over from the old Hugo head partial
- og-card.html now uses the self-hosted fonts and root-absolute paths, so
  a re-export cannot drift from the live typography

og-image.png is left as delivered: it is a text card where JPEG would show
artefacts, and only scrapers fetch it, so it costs page performance nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two jobs, deploy-pages gated on needs: validate. Pages must be set to
source "GitHub Actions", not "Deploy from a branch" — the branch mode
publishes every push regardless of whether the checks passed, which would
make the gate meaningless.

- design/check.js asserts the site-specific rules generic tools miss:
  canonical/og:url spelling incl. trailing slash, noindex on the imprint
  and 404 but not on / and /cv/, no third-party font references, no
  relative paths (they break the 404 page, which is served from arbitrary
  paths), every referenced file exists, manifest and sitemap contents,
  byte budgets
- html-validate with void-style/doctype-style off: those flag valid HTML5
  self-closing void elements, which is how the handoff wrote them
- linkinator for internal links only; external hosts are checked by hand
  before launch rather than flaking in CI forever
- axe over all four pages, Lighthouse over three with the median of three
  runs so a slow runner does not fail an unchanged site
- the artifact excludes design/ and the repo plumbing

deploy-pages is skipped, not failed, while vars.PAGES_ENABLED is unset —
Pages cannot serve from a private repo on the free plan, so the first run
has to be validate-only.

Actions are pinned to commit SHAs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 24x24 touch targets were added with padding alone, which grew the
layout boxes: the logo strip card measured 134px instead of the designed
122px and the footer 68px instead of 56px, because the "Full CV" and
imprint links each became 12px taller than their line box.

Padding still defines the hit area; a matching negative margin cancels its
effect on layout. Measured against the handoff reference served side by
side, every height now matches exactly:

  nav 66, logo strip 122, its label row 16, footer 56, CV facts row 41

Hit areas stay above the minimum: Full CV 71x28, footer 135x28, nav 25x30,
CV contact links 187x26.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first run failed on two SEO assertions, both for reasons that were not
defects:

- /impressum/ scores ~0.61 because it is deliberately noindex. Asserting
  SEO on a page we ask search engines to skip asserts the opposite of the
  intent, so that page now gets accessibility, best-practices and
  performance only. design/check.js already verifies the noindex itself.
- /cv/ scores 0.92 on link-text, which flags the nav label "Start" as
  generic. That word is authored copy from the handoff. Lighthouse judges
  the visible text, not the accessible name, so an aria-label does not
  satisfy it — tried and reverted, since it would have added an ARIA
  override that fixes nothing.

Turning link-text off caps the category at 0.92, so the category score
alone would no longer catch a real regression. The SEO audits that matter
are therefore asserted individually: canonical, document-title,
meta-description, http-status-code, is-crawlable, crawlable-anchors,
hreflang, image-alt.

Verified by running the extracted config through @lhci/cli locally against
all three pages: all assertions pass.

Whether "Start" should read differently is a copy decision, not a
technical one, and is left to Carsten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All 48 em dashes in the visible copy are gone, replaced by the punctuation
each position actually calls for rather than by a different dash. Code
comments and the README keep theirs.

- page and og titles: "Name | Descriptor", the separator the old Hugo site
  used as well
- image alt: "Carsten Bokemeyer: I build platforms as products, ..."
- appositives and trailing qualifiers: comma
- lead-ins to a list or an elaboration: colon (side projects intro, small
  builds, chefkoch.de portfolio, tech organization, EMEA/NAMER/APAC)
- the bracketed aside about the Platform & Ecosystem group headcount: real
  parentheses
- talk titles: colon before the quoted title, except "Working Products
  2019", whose title already contains a colon — a comma there avoids two
  stacked colons
- education rows: the space before the span was dropped so the colon sits
  tight against the year instead of reading " : Westdeutsche"
- contact paragraph: colon, since the preceding list is a noun phrase and a
  comma would splice it onto the imperative

An en dash was not used: the CV already spells date ranges with it, and
reusing the glyph would give one character two meanings.

Note: assets/og-image.png still shows the old headline. The template
design/og-card.html is updated, but the PNG has to be re-exported at
1200x630 for the two to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The social card still showed the em dash headline after the punctuation
change. Re-exported from design/og-card.html through headless Chrome, so
the real self-hosted webfonts are used rather than a hand-rebuilt image.

The card is 1200x630 inside a body with 20px padding, so the shot is taken
at 1240x670 and the padding cropped away centred. Result is byte-for-byte
the declared size and, at 180 KB, smaller than the 272 KB it replaces.

design/og-export.sh captures the recipe: it reuses the local preview server
if one is already running, starts one otherwise, and asserts the output
dimensions before it finishes.

Not changed: the card's footer still reads carsten-bokemeyer.de, which is
authored copy and the domain the site is headed for, though not the one it
is served from today. Noted in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card footer read carsten-bokemeyer.de, a domain that currently answers
403 from a different host. Anyone sharing the link today would see a
preview naming an address that does not serve this site.

It now reads carstenb.github.io. Re-exported with design/og-export.sh, so
the PNG and the template stay in step. Layout is unaffected: the string is
a similar length and the footer row does not rewrap.

This joins the list of things that change together when the custom domain
lands — canonicals, og:url and og:image, the sitemap, the JSON-LD @id, the
Search Console property, and now this line. Noted in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces "Start" in the nav of the CV, imprint and 404 pages. Lighthouse
counted "Start" among its generic link texts, and it judges the visible
text, so the CI exception that word forced can go: link-text is asserted
again and categories:seo is back to a minimum of 1.

The German imprint uses "Home" too, for consistency across the nav. Its
footer keeps "Zurück zur Startseite" — that is a sentence, not a nav
label, and "Zurück zu Home" would be poor German.

Also fixes design/check.js walking dot-directories: a local .lighthouseci
run left 800 KB report files that the byte budget then flagged as deployed
content. Tooling directories are now skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed performance on the start page at 0.86/0.85/0.84. It was not
runner noise, as I had assumed: CLS was 0.234, well past the 0.1 budget.

A real browser showed zero shift because the fonts were cached. Lighthouse
measures cold, so the swap actually happens — and only Manrope and
Gabarito were preloaded. Caveat Brush carries the headline accent and
Space Mono the labels, both above the fold on every page, so they arrived
late and reflowed the largest text on screen.

All four are now preloaded. They are needed above the fold regardless, so
this shifts their discovery earlier rather than adding weight.

  performance  0.84 -> 0.98
  CLS          0.234 -> 0
  FCP          1.5s -> 0.9s
  speed index  1.5s -> 0.9s
  LCP          2.7s -> 2.4s

Verified with the workflow's own config through @lhci/cli across all three
audited pages: all assertions pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. Privacy wording no longer contradicts the hosting section

   The old text claimed absolutely that no personal data is collected or
   stored, one section after stating that GitHub processes access data,
   and it did not cover incoming mail cleanly either. It now scopes the
   claim to what I actually control ("beim bloßen Besuch ... erhebe ich
   selbst keine"), names GitHub's technically required processing as
   unaffected, and covers correspondence explicitly.

   Still a draft: the legal text needs Carsten's sign-off before this goes
   public.

2. CI tooling pinned to exact versions

   The actions were pinned to commit SHAs, but the npx tools floated on
   version ranges, so re-running this commit later could validate it with
   different tools than it passed with. Added package.json with exact
   devDependencies plus package-lock.json; the workflow runs npm ci and
   invokes every tool with --no-install, which fails loudly instead of
   quietly fetching something newer.

   package.json, package-lock.json and node_modules are excluded from the
   Pages artifact, and the deploy job now asserts they are absent.

3. Print rule no longer treats the whole history as one block

   .roles wraps all five positions and runs to several pages, so
   break-inside:avoid on it asked for something impossible. Now only .role
   and .row avoid breaking.

design/check.js gained a NOT_DEPLOYED list so the byte budget measures the
artifact rather than the working tree — package-lock.json alone had pushed
the reported total from 795 KB to 1001 KB.

Verified locally with the pinned tools: invariants, html-validate,
linkinator (26 links), axe (4 pages, 0 violations), Lighthouse (9 runs, all
assertions), and the print rules checked by applying the @media print block
unconditionally: nav/skip/contact/footer hidden, .role and .row avoid,
.roles auto, company URLs printed, mailto suppressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces "carry" in the hero line, its two alt-text copies on each of the
three pages, and the OG card template — eight occurrences.

"carry" is idiomatic English in the sense of sustaining something, but it
reads as literal load-bearing to a German ear, and Carsten is the author.
"build" was ruled out: the sentence opens with "I build platforms", so
repeating the verb flattens the contrast and shifts the meaning from
operating to making. "run" is the standard word for operating a system in
production and keeps the original sense.

og-image.png re-exported so the card matches the page. Headline still sets
in three lines, the Caveat Brush accent still sits on "products".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@carstenb
carstenb merged commit c16b0af into master Sep 9, 2026
2 checks passed
@carstenb
carstenb deleted the redesign-2026 branch September 27, 2026 14:26
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