diff --git a/.gitignore b/.gitignore index 65fb2d465..6ebb52634 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ yarn-error.log* /test-results/ /playwright-report/ /playwright/.cache/ + +# Visual audit / scratch artifacts +/.scratch/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..ff5cedf2f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,44 @@ +# AGENTS.md + +Neumann newsroom theme — a fork of Prezly's `theme-nextjs-bea` (Next.js 15 App Router). Package name is still `theme-nextjs-bea`. Heavy brand customization lives alongside the upstream base. + +## Commands (pnpm — NOT npm) +Lockfile is `pnpm-lock.yaml`. `package.json` `engines` says npm, but always use **pnpm** (Node 20). + +- `pnpm dev` — runs plain `next` on **port 3000** (no port override exists; CLAUDE.md's "3003" is wrong). It will fall back to 3001/3002 if 3000 is taken. +- `pnpm check` — Biome lint + format check (this is the gate, not `lint` alone). +- `pnpm typecheck` — `tsc --noEmit --incremental`. +- `pnpm lint:fix` / `pnpm format:fix` — Biome auto-fix. +- `pnpm build` — production build. +- `pnpm test` — Playwright (`npx playwright test`); see Testing below. +- `pnpm visual:audit:live` / `:local` — screenshot diff tooling (`scripts/visual-audit.ts`, runs via `tsx`). + +Quality gate after any change: `pnpm typecheck` → `pnpm check` → `pnpm build`, all clean (warnings OK). + +## Verifying UI against the live source +This fork visually mirrors `https://newsroom.neumann.com/`. When matching layout/crop/spacing, measure the live source with Playwright (computed styles + `getBoundingClientRect`) before changing CSS — the source uses responsive `50vh`-style heights and `object-fit: cover`, which are easy to mis-replicate with a fixed `aspect-ratio`. + +## Testing +- Tests in `tests/` (`index.spec.ts`). `playwright.config.ts` reads `.env.local` and hits `TESTS_BASE_URL` (default `http://localhost:3000`). A server must be running and `.env.local` populated. +- CI `playwright.yml` only runs on Vercel `deployment_status` events, not on push. Push CI = lint only. + +## Environment +Copy `.env.example` → `.env.local`. Required: `PREZLY_ACCESS_TOKEN`, `PREZLY_NEWSROOM_UUID`, `MEILISEARCH_API_KEY`. `PREZLY_THEME_UUID` is preset. `.env.example` lists sample tokens. + +## Architecture (non-obvious bits) +- Pages live under `app/[localeCode]/` (the brackets are literal dirs; quote/escape them in shell globs). Server data via `app()` helper in `src/adapters/server/`. +- **`@prezly/theme-kit-nextjs` fetches live `ThemeSettings` from Prezly's backend at runtime and overrides `DEFAULT_THEME_SETTINGS`.** For fork-specific fonts/colors that must stick, hardcode them in `src/modules/Head/components/getCssVariables.ts` and the relevant component, not just in `DEFAULT_THEME_SETTINGS`. +- `next.config.js`: images use a custom Uploadcare loader; SVGs import as React components (`@svgr/webpack`); SCSS variables + mixins are injected globally via `prependData` (don't re-import them). Wrapper chain: `withBundleAnalyzer → withThemeKitConfig → withSentryConfig`. Sentry only enables in production with a DSN and not on Vercel. + +## Brand customizations (Neumann) +- Brand defaults are hardcoded in `src/theme-settings.ts` (black bg, orange `#ef7b0b` accent). `Font.FF_UNIT_PRO` is self-hosted from `public/fonts/neumann/` and declared in `src/styles/_fonts.scss`; `getGoogleFontName()` returns `null` for it. +- `custom/` is **flat files** (`NeumannFooter.tsx`, `NeumannCategoriesNav.tsx`, `*.module.scss`) imported via `@/custom/*` — not the deep barrel structure CLAUDE.md describes. `custom/neumann.scss` and `custom/fonts.css` referenced in CLAUDE.md do not exist. +- `middleware.ts` matcher already excludes `fonts/` and `*.woff2?` so self-hosted fonts aren't intercepted — preserve that if editing the matcher. + +## SCSS gotchas +- `src/styles/_fonts.scss` must be **CSS-only (no `@use`)**: `prependData` injects `@use` lines at the top of every SCSS file, and `@font-face`/CSS output must come after all `@use`. Putting `@use` in that file breaks the build. +- Biome only checks `app/**`, `src/**`, `custom/**` (see `biome.json`). Files outside those are unlinted. +- TS is strict with `noUnusedLocals`/`noUnusedParameters`. Use the `@/*` path aliases in `tsconfig.json` (e.g. `@/adapters/server`, `@/modules/*`, `@/custom/*`). + +## Related instruction files +`CLAUDE.md` (root + `~/.claude/CLAUDE.md`) has broader theme conventions, but several specifics are stale — trust this file and the executable config (package.json, next.config.js, middleware.ts) over CLAUDE.md where they conflict, especially the dev port and `custom/` layout. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..9c25efa32 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,110 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the **Neumann Newsroom Theme**, a customized Next.js 15 theme for the Prezly platform. It's based on the Prezly Bea Theme but heavily customized to match the Neumann brand identity (dark theme, orange accents). + +## Commands + +```bash +pnpm dev # Start development server (port 3003) +pnpm build # Production build +pnpm start # Start production server +pnpm typecheck # TypeScript type checking +pnpm check # Run all Biome checks (lint + format) +pnpm lint:fix # Fix linting issues +pnpm format:fix # Fix formatting issues +pnpm test # Run Playwright tests (requires dev server running) +``` + +## Architecture + +### Data Layer + +- **Prezly Theme Kit** (`@prezly/theme-kit-nextjs`): Core data-fetching abstraction for Prezly API +- **`src/adapters/server/`**: Server-side adapters for newsroom data, routing, metadata, analytics +- **`src/adapters/client/`**: Client-side adapters for HTTP, routing, locale, i18n +- **`app()` helper** (`src/adapters/server/app.ts`): Main server-side data access - `app().newsroom()`, `app().story()`, `app().stories()`, etc. + +### Routing + +- **App Router with locale prefix**: Routes under `app/[localeCode]/` +- **Route definitions**: `src/adapters/server/routing.ts` +- **Middleware** (`middleware.ts`): Locale detection via `IntlMiddleware` + +### Key Routes +- `(index)/` - Homepage with story listings +- `(story)/[slug]/` - Individual story pages +- `category/[slug]/` - Category pages +- `media/` - Media gallery pages +- `search/` - Search page + +### Neumann Customizations + +This theme has been customized for the Neumann brand. Key customization points: + +1. **Theme Settings** (`src/theme-settings.ts`): Hardcoded Neumann brand colors and settings + - Does NOT pull from Prezly API - uses `DEFAULT_THEME_SETTINGS` only + - Colors: Black background (#000000), orange accent (#ef7b0b), gray text (#c1c1c1) + +2. **Custom Font** (`custom/fonts.css` and `custom/fonts/`): + - FF Unit Pro font (Neumann's brand font) loaded locally + - Three weights: 300 (light), 400 (regular), 700 (bold) + - Imported in layout.tsx + +3. **Custom SCSS** (`custom/neumann.scss`): Brand-specific style overrides + - Typography with FF Unit Pro font + - Card styling with orange titles + - Navigation styling (uppercase links) + - Hero image fix for positioning + - Footer styling + - Imported in `src/styles/styles.globals.scss` + +4. **Custom Header Navigation** (`custom/NeumannCategoriesNav.tsx`): + - Shows categories as direct links (Press, Stories) instead of dropdown + - Sennheiser external link configured via `main_site_url` setting + +5. **Custom Footer** (`custom/NeumannFooter.tsx`): + - 3-column layout: Company, Service & Support, Products + - Matches the original Neumann newsroom footer + - Copyright and legal links at bottom + +6. **Modified Header Component** (`src/modules/Header/ui/Header.tsx`): + - Uses `NeumannCategoriesNav` instead of default `Categories` component + - Removed categories bar layout - categories always in header + +### Path Aliases + +Defined in `tsconfig.json`: +- `@/adapters/server`, `@/adapters/client` - Data layer +- `@/components/*`, `@/modules/*` - UI code +- `@/hooks`, `@/utils`, `@/icons` - Utilities +- `@/custom/*` - Neumann-specific customizations +- `@/theme-settings` - Theme configuration + +### Styling + +- SCSS Modules with variables in `src/styles/variables/` +- Global SCSS imports auto-prepended via `next.config.js` +- CSS variables generated from theme settings in `src/modules/Head/components/getCssVariables.ts` + +### Environment Variables + +Required in `.env.local`: +- `PREZLY_ACCESS_TOKEN` - API access token +- `PREZLY_NEWSROOM_UUID` - Newsroom identifier +- `PREZLY_THEME_UUID` - Theme preset identifier (use `73015107-ac86-418b-9120-4ffa439d5c0f`) +- `MEILISEARCH_API_KEY` - For search functionality + +## Neumann Brand Colors + +| Purpose | Color | Hex | +|---------|-------|-----| +| Background | Black | #000000 | +| Card Background | Dark Gray | #2d2d2d | +| Accent/Links | Orange | #ef7b0b | +| Primary Text | White | #ffffff | +| Secondary Text | Gray | #c1c1c1 | +| Tertiary Text | Light Gray | #cccccc | diff --git a/NEUMANN_CUSTOMIZATION_PLAN.md b/NEUMANN_CUSTOMIZATION_PLAN.md new file mode 100644 index 000000000..01681efe2 --- /dev/null +++ b/NEUMANN_CUSTOMIZATION_PLAN.md @@ -0,0 +1,239 @@ +# Neumann Theme Customization Plan + +Living document tracking the visual customization of this Next.js theme to +match the live target at . + +> Status: **In progress**. Phases get checked off as they're completed. +> See `VISUAL_AUDIT.md` for the headless-browser tooling that drives the +> visual verification loop. + +## Visual target + +Source of truth: . Reference screenshots live +in `.scratch/screens/live/` (gitignored, regenerate with `pnpm visual:audit:live`). + +### Brand tokens + +**Colors** + +| Role | Hex | +|------|-----| +| Page background | `#000000` | +| Card / dropdown background | `#2d2d2d` | +| Mobile drawer background | `#1c1c1c` | +| CTA button background | `#141414` | +| Accent / brand orange | `#ef7b0b` | +| Accent hover (lighter) | `#f18823` | +| Accent active (darker) | `#ef7b0a` | +| Body text | `#c1c1c1` | +| Headings / strong text | `#ffffff` | +| Secondary text / dates | `#cccccc` | + +**Typography** — FF Unit Pro (300 / 400 / 700), self-hosted. +Body 15px, line-height ~1.5. Sentence case headings. UPPERCASE with +letter-spacing on nav, "Read more", date labels, footer h5. + +### Component-level spec (verified from live DOM/CSS) + +- **Header** — Black, max-width 1600px container, ~88px tall. Logo left + (61px desktop / 40px mobile). Right cluster: nav (UPPERCASE: PRESS · STORIES + · SENNHEISER) → orange search icon → globe + locale label + caret. + Sticky after `pageYOffset > 110`. +- **Search drawer** — Fixed at top of viewport, full width, 110px desktop / + 60px mobile. `background: rgba(0,0,0,0.85)`, `border-bottom: 3px solid + #ef7b0b`. Just an input + close button — submits a GET to `/search?query=…`. + **Replaces the current `` modal.** +- **Hero** — Full-bleed image, height `80vh / max 660px` desktop, 500px tablet, + 420px mobile. 25% black overlay on image. Centred translucent box + `rgba(0,0,0,0.5)`, `padding: 80px 40px`, `max-width: 40vw` desktop. Title + white ~28–32px, subtitle grey 18px, then `MORE >` button (`#141414` bg, white + border, hover orange border + text). +- **Story grid** — 3 cols desktop / 2 tablet / 1 mobile. Card `#2d2d2d`, no + border-radius, image top (square-ish 1.2:1, ~300px tall desktop), content + area `padding: 32px 24px 76px`. Title sentence case 20px / 1.33. Date + UPPERCASE 14px `letter-spacing: 2px`. "READ MORE >" UPPERCASE 16/500 chevron. +- **Footer** — 3 columns: **Company / Service & Support / Products**. Centered + Neumann.Berlin logo + 3 social icons (Facebook / Instagram / YouTube). Bottom + strip: `© 2018-{year} Georg Neumann GmbH` + 7-item legal menu. +- **Search results page** — "Search results" 32/48px centered with + `linear-gradient(180deg, #2d2d2d, #000)` top fade. Two-column: results + + facet sidebar (Category checkboxes only, Year facet deferred). + +## Confirmed decisions + +| # | Question | Decision | +|---|----------|----------| +| Q1 | `main_site_url` / `main_site_label` source | **Hardcoded for now** (in foundation layer) | +| Q2 | Search drawer fidelity | **A — simple form, GET to /search** (no live results) | +| Q3 | `/search` Year facet | **Defer** — Category facet styling only | +| Q4 | Language selector visual | Style only, no rewrite | +| Q5 | Hero full-bleed enforcement | **Force on** — drop the toggle path | +| Q6 | Hub-mode handling | **Skip** — assume non-hub | + +## Architectural decisions + +- **D1.** Hardcode Neumann brand values in + `src/modules/Head/components/getCssVariables.ts` so the Prezly API's live + `themeSettings` cannot override them. `DEFAULT_THEME_SETTINGS` is also + updated for type safety / fallback. +- **D2.** Move `custom/fonts/*.{woff,woff2}` to `public/fonts/`. Define + `@font-face` in a new `src/styles/_fonts.scss` (forwarded from + `styles.globals.scss`). Update `middleware.ts` matcher to exclude + `fonts/|.*\\.woff2?$`. Delete `custom/fonts.css` (dead code). +- **D3.** Replace `` modal with a new `custom/NeumannSearchDrawer/` + — fixed top, simple form (Q2 → A). The InstantSearch live-results widget + is dropped; submitting goes to the existing `/search` page. +- **D4.** Wire the existing `custom/NeumannFooter.tsx` into + `app/[localeCode]/layout.tsx`, replacing the base `