integration: add Playwright, axe and Lighthouse CI gates

Lock SEO, accessibility and crawlability with headless browser checks so regressions fail before a merge instead of after publication.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-25 18:28:24 +02:00
parent 46c86447d1
commit 34367181d2
16 changed files with 3715 additions and 22 deletions

View File

@@ -42,6 +42,9 @@ This is a bilingual recruiter and B2B portfolio for a software/DevOps engineer.
| `npm run format:check` | Prettier check (CI). |
| `npm run serve:ssr` | Serve the production SSR bundle. |
| `npm run serve:ssr:Portfolio` | Alias of `serve:ssr` for existing tooling. |
| `npm run e2e` | Playwright + axe-core against the SSR bundle. |
| `npm run e2e:install` | Install the pinned Chromium build. |
| `npm run lighthouse` | Production build, then Lighthouse CI. |
| `npm run ci` | lint, format:check, test:ci, then build. |
## Code conventions
@@ -99,6 +102,7 @@ German and English are written idiomatically per language, never machine-transla
- No `window`, `document`, `navigator`, `localStorage` or `matchMedia` access outside `afterNextRender` or `isPlatformBrowser` guards
- Use the injected `DOCUMENT` token when the document element is required (it works on server and browser)
- No user-agent sniffing; use CSS media queries for device capability
- Exception: `isApplePlatform()` in `src/app/core/platform/browser.ts` may read `navigator.userAgentData?.platform ?? navigator.platform` after hydration, only to label the Command key. There is no CSS media query for that key. It is not used for device capability.
- Capability helpers live in `src/app/core/platform/browser.ts` and return conservative defaults on the server
- Helpers that use `inject()` must be called from a field initializer or constructor, never from a lifecycle hook or callback, and the resolved value must be stored on the instance
- Every addressable route must remain prerenderable
@@ -121,8 +125,13 @@ German and English are written idiomatically per language, never machine-transla
- Real locale-prefixed URLs
- Server-rendered core text
- Per-route titles from the content layer
- Canonical origin is `SITE_CONFIG.siteOrigin` (`https://antoniolede.de`); every absolute URL in the app and tests is derived from it
- Per-route metadata (description, canonical, reciprocal hreflang including `x-default`, Open Graph, Twitter, robots) is written by `SeoService` during SSR and on every client navigation
- One JSON-LD `@graph` script per route: Person and ProfessionalService on Home, Service on service routes, CreativeWork per public case on projects, WebPage elsewhere
- `public/robots.txt`, `public/sitemap.xml` and `public/llms.txt` stay synchronized with `prerenderablePaths()` via `crawl-assets.spec.ts`
- The language switch exposes `hreflang` on the alternate-locale link
Canonical, hreflang document tags and JSON-LD arrive in the integration phase. The language switch already exposes `hreflang` on the alternate-locale link.
Browser binaries, HTML reports, traces and screenshots are not committed (`test-results`, `playwright-report`, `.lighthouseci`).
## Test checklist
@@ -136,6 +145,8 @@ A change is not done until:
6. Routing and locale contracts still have unit coverage (paths, locale helpers, navigation links)
7. No new `window` / `document` / `navigator` reads were added outside an SSR-safe guard
8. Placeholder or legal copy was not replaced with invented professional claims
9. Crawl files still match `SITE_CONFIG.siteOrigin` and `prerenderablePaths()`
10. Playwright (`npm run e2e`) and Lighthouse CI (`npm run lighthouse`) still pass when the change affects public HTML, metadata or chrome
## Domain boundaries