# AGENTS.md Contributor and agent guide for this repository. Read this before changing routing, copy, styles, or SSR behaviour. ## Purpose This is a bilingual portfolio for a software/DevOps engineer. German is the default language and is served at `/`. English is the full second version and is served under `/en`. Home is the direct-customer experience. `/pitch` is the recruiter-oriented page and is reachable without sitting in the primary navigation. Every public page exists in both locales. ## Architecture - Angular 21 standalone components, no NgModules - Zoneless change detection - Signals for local state - Lazy-loaded feature routes - SSR with prerendering (`@angular/ssr`, `outputMode: "server"`) - Express host in `src/server.ts` ## Directory map | Path | Role | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `src/app/core/**` | Contracts, services and tokens. No UI. | | `src/app/features/**` | Routed page components, one folder per route. | | `src/app/shared/**` | Reusable UI primitives used by multiple features. | | `src/app/components/**` | Existing decorative/feature widgets (skills grid, dot background). | | `public/**` | Static SVG and public assets copied as-is. | | `src/styles.scss` | Global design tokens and primitives. The only place raw brand hex values are defined. | | `src/_breakpoints.scss` | Breakpoint map (`sm` 40rem, `md` 48rem, `lg` 64rem, `xl` 80rem) and the `respond-to` mixin. Components pull it in with `@use 'breakpoints'`. | ## Commands | Script | Purpose | | ----------------------------- | --------------------------------------------- | | `npm start` | Dev server (`ng serve`). | | `npm run build` | Production build (default configuration). | | `npm run watch` | Development rebuild on change. | | `npm test` | Unit tests in watch mode. | | `npm run test:ci` | Unit tests once (`--watch=false`); must exit. | | `npm run lint` | ESLint over the workspace. | | `npm run lint:fix` | ESLint with autofix. | | `npm run format` | Prettier write. | | `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 - Standalone components only - `inject()` over constructor injection for new code - Signals over RxJS for local state - `ChangeDetectionStrategy.OnPush` on new components - kebab-case file names - No `any` - No default exports - Templates in separate files ## SCSS conventions - Global design tokens live only in `src/styles.scss` as CSS custom properties - Components consume `var(--...)` and never redefine raw hex values - Use the documented spacing (`--space-1` … `--space-12`) and typography (`--text-xs` … `--text-3xl`) scale - Hover-only affordances must be wrapped in `@media (hover: hover) and (pointer: fine)` - All motion must respect `prefers-reduced-motion` - Breakpoints and the `respond-to` mixin live in `src/_breakpoints.scss`; do not invent extra breakpoint names - Shared glass, layout and focus treatments belong on the global primitives (`.glass-surface`, `.content-container`, `.stack`, `.cluster`), not copied into component stylesheets - Keep component styles under the `anyComponentStyle` budget (4 kB warn, 8 kB error) ## Content rules All user-facing copy comes from the typed bilingual content layer in `src/app/core/content`. Never inline marketing or page copy in templates. Keep the four copy roles as separate fields: - `headline` — the serious heading - `proof` — only a verifiable statement - `playfulLine` — optional wordplay; never a capability claim - `cta` — action label, kept swappable German and English are written idiomatically per language, never machine-translated word for word. ## Claims policy (non-negotiable) - No unsupported superlatives - No invented references, client names, metrics or certifications - Unproven AI, local-LLM or multi-agent capabilities are phrased as an offering or way of working, never as a delivered reference - The CV stations HUP, BitWiz and Cybertrading are deliberately excluded from all public pages, cases and timelines - The legal pages (Impressum, Datenschutz) contain placeholders that MUST be reviewed by the site owner before publication and must visibly say so ## i18n rules - German at `/`, English under `/en` - Primary navigation is Home, Services, Projects, About and Contact. Services is one anchored page with four sections. Stack and Pitch stay reachable from content and the terminal, not from the primary nav. - Every route id exists in both locales - New pages are added by extending the route-id table in `src/app/core/routing`, never by hard-coding paths - Do not swap `LOCALE_ID`; the active locale is `AppLocale` from `LocaleService` ## SSR rules - 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 ## Accessibility checklist - Skip link as the first focusable element, targeting `#main-content` - Single `h1` per page - Landmark elements (`header`/`nav`, `main`, `footer`) - Visible `:focus-visible` ring - Keyboard reachability for every action - `aria-current` on the active nav item - At least 4.5:1 text contrast on the base surface - Reduced-motion alternatives for animation and smooth scrolling - Navigation remains usable in its server-rendered default state (no JS-only menus) ## SEO expectations - Semantic headings - 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, one Service entry per services section on the services route, 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 Browser binaries, HTML reports, traces and screenshots are not committed (`test-results`, `playwright-report`, `.lighthouseci`). ## Test checklist A change is not done until: 1. `npm run lint` exits 0 2. `npm run format:check` exits 0 3. `npm run test:ci` exits 0 with no watcher left running 4. `npm run build` exits 0 5. New or changed routes appear in `prerenderablePaths()` and are prerendered 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 | Branch | Owns | | ----------- | -------------------------------------------------------------------------------- | | Foundation | Tooling, tokens, shell, routing and content contracts, SSR safety, test baseline | | Content | Bilingual copy data and page composition | | Signature | Systems Map, terminal dock, dot background and motion | | Integration | SEO and cross-cutting a11y/performance hardening | Each branch extends the shared contracts instead of duplicating paths or copy. The Content branch owns the bilingual copy tree behind `SITE_CONTENT`. The Signature branch may replace the dot-background internals but must keep the SSR-safe init/teardown contract.