Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
126 commits
Select commit Hold shift + click to select a range
6d2a977
docs: add CLAUDE.md with project architecture and commands
jakubferenc Jun 2, 2026
b9aea9a
docs(plan): add Nuxt 2->4 migration implementation plan
jakubferenc Jun 6, 2026
7834123
chore(commit): pin commit command to Haiku; drop Nuxt 5 note from plan
jakubferenc Jun 6, 2026
b4f6583
docs(plan): rework Nuxt 4 migration for SSR, Composition API, and tes…
jakubferenc Jun 6, 2026
035e046
docs(claude): add working agreements (no auto-commit, yarn)
jakubferenc Jun 6, 2026
6759766
feat: migrate to Nuxt 4 SSR + Vue 3 Composition API
jakubferenc Jun 6, 2026
314d3dd
fix(nuxt4): make SSR build and i18n head render correctly
jakubferenc Jun 6, 2026
52ae88d
chore(dev): fix macOS vite-node socket path and set compatibilityDate
jakubferenc Jun 6, 2026
c51b834
test: add Vitest unit/component and Playwright e2e suites
jakubferenc Jun 6, 2026
c10da4d
refactor(imports): use @/ alias for source-root imports
jakubferenc Jun 6, 2026
46b4a73
docs: rewrite CLAUDE.md for Nuxt 4 SSR architecture
jakubferenc Jun 6, 2026
282242e
chore: remove unused scripts
jakubferenc Jun 6, 2026
43cc0b5
feat(mock): serve the site from local mock data
jakubferenc Jun 6, 2026
c903082
feat(homepage): rebuild index.vue with structured sections
jakubferenc Jun 6, 2026
b51fcbd
docs(wordpress): add setup guide for the structured-content model
jakubferenc Jun 6, 2026
1353873
feat(page-detail): render detail pages with structured section compon…
jakubferenc Jun 12, 2026
81ffbe2
feat(graphql): add codegen toolchain and migrate REST URL config
jakubferenc Jun 12, 2026
6af4949
feat(homepage): fetch upcoming program events via GraphQL
jakubferenc Jun 12, 2026
2a9295d
chore: gitignore generated TS types, add prod API env, add design spec
jakubferenc Jun 12, 2026
03f49c9
feat(header): migrate Header.vue from REST to GraphQL
jakubferenc Jun 12, 2026
8678d19
refactor(graphql): update field names to match renamed CPTs
jakubferenc Jun 12, 2026
7506dd2
refactor(homepage): rename upcomingProgramy → upcomingEvents; fix api…
jakubferenc Jun 12, 2026
07ba72d
refactor(store): trim Pinia to global data only; pages fetch directly
jakubferenc Jun 28, 2026
ae67c4a
fix config missing variable
jakubferenc Jun 29, 2026
6b0b540
refactor(header): extract parseUrl to utils/menu; fix hash + https bug
jakubferenc Jun 29, 2026
a8c2760
feat(models): move codegen output to models/graphql.generated.ts
jakubferenc Jun 29, 2026
72b17bb
feat(models): add WordPress REST API type interfaces
jakubferenc Jun 29, 2026
5222ff1
refactor(utils): convert graphql.js to strict TypeScript
jakubferenc Jun 29, 2026
21395d7
refactor(utils): convert functions.js to strict TypeScript
jakubferenc Jun 29, 2026
f9e2735
fix(utils): revert getOpenGraphMeta parameter to required per spec
jakubferenc Jun 29, 2026
488d06d
refactor(ts): type pages, components, store, factories, and config
jakubferenc Jun 29, 2026
72241e6
style: use === and ?? where behavior-preserving
jakubferenc Jun 29, 2026
02dbee8
docs: add TypeScript migration implementation plan
jakubferenc Jun 29, 2026
189b4d7
chore: add eslint and ts configs with best practices for our tech stack
jakubferenc Jun 29, 2026
69a19db
docs: WordPress content migration spec + plan
jakubferenc Jun 30, 2026
65ea606
feat(footer): render footer logos from footer_logo CPT via GraphQL
jakubferenc Jun 30, 2026
1bc2c98
feat(content): migrate pages/posts to GraphQL content + ACF
jakubferenc Jun 30, 2026
4a49976
feat(home): homepage editorial content from ACF via GraphQL
jakubferenc Jun 30, 2026
89d86f4
feat(program): migrate program/course pages to GraphQL
jakubferenc Jun 30, 2026
5e725bf
refactor(api): retire all REST factories — frontend is 100% GraphQL
jakubferenc Jun 30, 2026
d83f131
test(e2e): playwright visual baseline snapshots for migrated routes
jakubferenc Jun 30, 2026
83fcdbf
refactor: address final review — type-safe category compare, prune de…
jakubferenc Jun 30, 2026
eed40ab
feat(home): hero from ACF; program heading + features fixed in template
jakubferenc Jun 30, 2026
902f0ba
feat(homepage): add hero caption left/right alignment
jakubferenc Jun 30, 2026
ab5aef5
fix(homepage): place hero caption with flex, keep text left-aligned
jakubferenc Jun 30, 2026
1a27a63
feat(homepage): admin-overridable feature rows with default fallback
jakubferenc Jun 30, 2026
510db20
fix(types): make MetaTag assignable to useHead meta
jakubferenc Jun 30, 2026
14c7105
refactor(homepage): merge homepage query into index.graphql
jakubferenc Jun 30, 2026
84def33
feat(homepage): resolve hero caption link by type
jakubferenc Jun 30, 2026
13e3ea0
feat(pages): render centered h1 page title on detail pages
jakubferenc Jun 30, 2026
58a8ca5
feat(header): drive in-page section anchors from page data
jakubferenc Jun 30, 2026
6f14151
fix(header): query submenuFields on page/post only
jakubferenc Jun 30, 2026
9e8a6d5
feat(homepage): resolve feature links from internal nodes
jakubferenc Jun 30, 2026
3f7c98e
refactor(pages): drop stripLeadingH1; render title via PageTitle only
jakubferenc Jun 30, 2026
dd13ee8
refactor(o-nas): use PageTitle component for the page title
jakubferenc Jun 30, 2026
6d7478d
refactor(pages): inline the title h1, drop PageTitle component
jakubferenc Jun 30, 2026
5f8c175
fix(hydration): resolve mismatches that duplicated the page h1
jakubferenc Jun 30, 2026
348e6c0
feat(pages): respect per-page render-title toggle
jakubferenc Jun 30, 2026
35c667b
feat(pages): render reorderable flexible-content sections
jakubferenc Jun 30, 2026
ecfd7f4
refactor(pages): drop pageContentFields from post/event/course detail
jakubferenc Jun 30, 2026
90e8617
feat(o-nas): load "Ke stažení" files from WordPress via GraphQL
jakubferenc Jun 30, 2026
18bfd45
feat(o-nas): load Výroční zprávy from WordPress via GraphQL
jakubferenc Jun 30, 2026
fc60aff
feat(o-nas): load "Podpořte nás" from WordPress WYSIWYG via GraphQL
jakubferenc Jun 30, 2026
3302ff1
build(tailwind): safelist classes used in WordPress WYSIWYG content
jakubferenc Jun 30, 2026
aa5b56d
refactor(slider): extract reusable useSlider composable + Slider comp…
jakubferenc Jun 30, 2026
3e1a744
fix(o-nas): center subsection titles via Tailwind text-center
jakubferenc Jul 1, 2026
6a64b40
feat(o-nas): render contact tables from WordPress ACF
jakubferenc Jul 1, 2026
fe1f860
style(css): make wp-block column/alignment classes work in the FE
jakubferenc Jul 1, 2026
d9a028d
style(o-nas): match production heading typography
jakubferenc Jul 1, 2026
57ebea1
style(o-nas): center the Tým / Další kontakty tables
jakubferenc Jul 1, 2026
acbbd1a
refactor(o-nas): read contact tables from the contacts section
jakubferenc Jul 1, 2026
f12475c
refactor(header): read submenu anchors from submenuFields.nodes
jakubferenc Jul 1, 2026
5ae403f
feat(pages): migrate o-nas to [slug].vue via ACF-driven sections
jakubferenc Jul 1, 2026
15e0942
fix([slug]): remove excerpt field — Page type has no excerpt in WPGra…
jakubferenc Jul 1, 2026
c640f2c
feat(about-en): translate English About page; add e2e smoke tests
jakubferenc Jul 1, 2026
673fcbd
feat(sections): add labeled_section ACF layout and LabeledSection com…
jakubferenc Jul 1, 2026
e1636a2
chore(vscode): enable ESLint fix-on-save
jakubferenc Jul 4, 2026
8297605
docs(eslint): note @nuxt/eslint applies Vue's flat/recommended tier
jakubferenc Jul 4, 2026
2008f7e
docs(divadlo): spec + implementation plan for divadlo/theatre section…
jakubferenc Jul 4, 2026
8d13dc1
feat(contacts): optional intro block between heading and table
jakubferenc Jul 4, 2026
047daa5
feat([slug]): thread contacts intro through query and mapping
jakubferenc Jul 4, 2026
e3b6f60
feat(wysiwyg): shared large-type prose mixin + largeType prop
jakubferenc Jul 4, 2026
b3daaac
chore: enforce v-if as first attribute
jakubferenc Jul 4, 2026
8cd70b7
style(redesign): eslint attribute formatting; add newsletter link to …
jakubferenc Jul 4, 2026
d3edb5c
refactor(contacts): extract linkifyEmails into shared utils
jakubferenc Jul 4, 2026
70d1fd1
feat(program-detail): two-column detail rendering the left-column field
jakubferenc Jul 5, 2026
5867b08
fix: jumping hero image
jakubferenc Jul 5, 2026
b5ec9b7
style(redesign): remove bold weight from h1 and h2 headings
jakubferenc Jul 5, 2026
164febf
style(redesign): tighten heading letter-spacing to -0.03em
jakubferenc Jul 5, 2026
6595e5f
refactor(pages): replace intro spacer with page-detail padding-top
jakubferenc Jul 5, 2026
d3b0d95
chore(tailwind): safelist ProseBody utilities for WYSIWYG content
jakubferenc Jul 5, 2026
75d61d6
chore: update claude md
jakubferenc Jul 5, 2026
018cc95
feat(program): add program page-section with filtering and hover hero
jakubferenc Jul 5, 2026
10ec256
chore(workspace): add cSpell words to fullstack workspace
jakubferenc Jul 5, 2026
2fb7125
feat(program): show program events on English pages too
jakubferenc Jul 6, 2026
9364e6a
chore(scripts): add cleanup and restart-clean yarn scripts
jakubferenc Jul 6, 2026
7cb12fb
fix: edit kreativ bubec page
jakubferenc Jul 6, 2026
4841de6
chore(i18n): reword empty-program message (cs)
jakubferenc Jul 8, 2026
fbcdfd4
feat(program): redesign event filters with multiselect and calendar/l…
jakubferenc Jul 8, 2026
477a55a
feat(program): sync filter state to the URL query string
jakubferenc Jul 8, 2026
603e5cd
ci(deploy): add GitHub Actions pipeline to san04 beta server
jakubferenc Jul 10, 2026
6483c14
docs(readme): rewrite README for the current Nuxt 4 SSR stack
jakubferenc Jul 10, 2026
81ca118
chore: use custom manual eslint
jakubferenc Jul 10, 2026
aacb338
refactor(types): replace explicit any with generated GraphQL types
jakubferenc Jul 10, 2026
ce9120f
refactor(types): simplify types and get typecheck to zero errors
jakubferenc Jul 10, 2026
83307c8
chore(env): point env files at beta backend, track .env.staging
jakubferenc Jul 10, 2026
4894b95
ci(deploy): consolidate CI steps into build:ci
jakubferenc Jul 10, 2026
40e2197
ci(deploy): split pipeline into build/test/deploy jobs
jakubferenc Jul 10, 2026
14e5038
fix(deploy): fix hidden artifact upload and missing .nuxt tsconfig
jakubferenc Jul 10, 2026
a97a177
test(program): rewrite stale ProgramList/ProgramFilters component tests
jakubferenc Jul 10, 2026
e5a8580
fix(deploy): bind nuxt container to loopback only
jakubferenc Jul 10, 2026
a99e8a9
test(e2e): fix remaining CI-only e2e failures
jakubferenc Jul 10, 2026
1bd0c1a
test(e2e): skip visual.spec.ts on CI — no Linux baselines exist
jakubferenc Jul 10, 2026
ec67f22
ci(deploy): stop gating deploy on test-e2e
jakubferenc Jul 10, 2026
a85e522
chore(config): bump production swr to 30s, default GraphQL fallback t…
jakubferenc Jul 11, 2026
24ddba0
chore: remove netlify from repo
jakubferenc Jul 11, 2026
afaa2ff
feat(footer-logo): add cssClass and htmlId ACF fields
jakubferenc Jul 11, 2026
6607ddb
feat(footer-logo): add heightPx/widthPx fields for inline-style sizing
jakubferenc Jul 11, 2026
4f2c445
docs: default commit messages to subject-line only
jakubferenc Jul 11, 2026
ac3b2c0
chore: add remote gql running of dev
jakubferenc Jul 12, 2026
1e7a228
fix(e2e): fix e2e smoke tests
jakubferenc Jul 12, 2026
b3308ef
fix(scripts): restore build and dev:mock scripts
jakubferenc Jul 12, 2026
3325fb0
feat(sentry): add basic error tracking
jakubferenc Jul 12, 2026
4d90c22
ci(deploy): require test-e2e to pass before deploy
jakubferenc Jul 12, 2026
6bdd9d9
feat(newsletter): add GDPR consent form and privacy policy page
jakubferenc Jul 13, 2026
a90f0ae
feat(newsletter): wire form to WP subscribe endpoint + double opt-in
jakubferenc Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .claude/commands/commit.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
model: claude-haiku-4-5-20251001
---

# Commit

Create a git commit following conventional commit syntax and best practices.
Expand Down Expand Up @@ -41,8 +45,7 @@ Create a git commit following conventional commit syntax and best practices.
- `<scope>` is optional but encouraged — use the affected module, file, or area (e.g. `api`, `auth`, `build`)
- `<subject>` is lowercase, imperative mood, no period at end (e.g. `add retry logic`, not `Added retry logic.`)
- Keep the subject line under 72 characters
- Body lines wrap at 72 characters
- Use the body to explain *why*, not *what* — the diff shows what
- **Default to subject-line only — no body.** Only add a body for a genuinely important or non-obvious commit (e.g. a breaking change, a non-obvious workaround, a fix whose cause isn't visible in the diff), and keep it to one short line, not a multi-paragraph explanation. The diff already shows *what* changed — don't restate it.
- Breaking changes: add `!` after type/scope (e.g. `feat(api)!:`) AND a `BREAKING CHANGE:` footer

**Examples:**
Expand Down
7 changes: 7 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NPM_FLAGS=--no-optional

WP_REST_URL=https://bubec-wordpress-beta.jakubferenc.cz
WP_GRAPHQL_URL=https://bubec-wordpress-beta.jakubferenc.cz/wordpress/graphql

SENTRY_DSN=https://5c970e152985be7b91255ee405917532@o275554.ingest.us.sentry.io/4511484951658496
SENTRY_ENVIRONMENT=production
7 changes: 7 additions & 0 deletions .env.staging
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
NPM_FLAGS=--no-optional

WP_REST_URL=https://bubec-wordpress-beta.jakubferenc.cz
WP_GRAPHQL_URL=https://bubec-wordpress-beta.jakubferenc.cz/wordpress/graphql

SENTRY_DSN=https://5c970e152985be7b91255ee405917532@o275554.ingest.us.sentry.io/4511484951658496
SENTRY_ENVIRONMENT=staging
188 changes: 188 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Deploy to beta

on:
push:
branches: [feature/redesign-2026]
workflow_dispatch: {}

concurrency:
group: deploy-beta
cancel-in-progress: true

env:
DEPLOY_PATH: /www/hosting/jakubferenc.cz/bubec/nuxt

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable corepack
run: corepack enable

- name: Install dependencies
run: yarn install --immutable

- name: Use production env file
run: cp .env.production .env

- name: Build
run: yarn build:ci

- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: build-output
path: .output/
retention-days: 1
include-hidden-files: true

test-unit:
needs: build
if: vars.SKIP_TESTS != '1'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable corepack
run: corepack enable

- name: Install dependencies
run: yarn install --immutable

- name: Prepare Nuxt types
run: yarn nuxt prepare

- name: Unit tests
run: yarn test:unit

test-component:
needs: build
if: vars.SKIP_TESTS != '1'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable corepack
run: corepack enable

- name: Install dependencies
run: yarn install --immutable

- name: Prepare Nuxt types
run: yarn nuxt prepare

- name: Component tests
run: yarn test:component

test-e2e:
needs: build
if: vars.SKIP_TESTS != '1'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24

- name: Enable corepack
run: corepack enable

- name: Install dependencies
run: yarn install --immutable

- name: Prepare Nuxt types
run: yarn nuxt prepare

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Download build output
uses: actions/download-artifact@v4
with:
name: build-output
path: .output/

- name: E2E tests
run: yarn test:e2e

deploy:
needs: [test-unit, test-component, test-e2e]
if: |
always() &&
(
vars.SKIP_TESTS == '1' ||
(
needs.test-unit.result == 'success' &&
needs.test-component.result == 'success' &&
needs.test-e2e.result == 'success'
)
)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Download build output
uses: actions/download-artifact@v4
with:
name: build-output
path: .output/

- name: Install sshpass
run: sudo apt-get update && sudo apt-get install -y sshpass

- name: Ensure deploy path exists
env:
SSHPASS: ${{ secrets.DEPLOY_SSH_PASSWORD }}
run: |
sshpass -e ssh -o StrictHostKeyChecking=no \
"${{ secrets.DEPLOY_SSH_USER }}@${{ secrets.DEPLOY_SSH_HOST }}" \
"mkdir -p ${{ env.DEPLOY_PATH }}"

- name: Rsync build output
env:
SSHPASS: ${{ secrets.DEPLOY_SSH_PASSWORD }}
run: |
sshpass -e rsync -avr --delete \
-e "ssh -o StrictHostKeyChecking=no" \
.output/ "${{ secrets.DEPLOY_SSH_USER }}@${{ secrets.DEPLOY_SSH_HOST }}:${{ env.DEPLOY_PATH }}/.output/"

- name: Rsync deploy config
env:
SSHPASS: ${{ secrets.DEPLOY_SSH_PASSWORD }}
run: |
sshpass -e rsync -av \
-e "ssh -o StrictHostKeyChecking=no" \
Dockerfile.deploy docker-compose.deploy.yml \
"${{ secrets.DEPLOY_SSH_USER }}@${{ secrets.DEPLOY_SSH_HOST }}:${{ env.DEPLOY_PATH }}/"

- name: Restart container on server
env:
SSHPASS: ${{ secrets.DEPLOY_SSH_PASSWORD }}
run: |
sshpass -e ssh -o StrictHostKeyChecking=no \
"${{ secrets.DEPLOY_SSH_USER }}@${{ secrets.DEPLOY_SSH_HOST }}" \
"cd ${{ env.DEPLOY_PATH }} && docker compose -f docker-compose.deploy.yml up -d --build"
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ yarn-error.log*
.yarn/build-state.yml
.yarn/install-state.gz

# Design
resources/design

# Runtime data
pids
*.pid
Expand Down Expand Up @@ -107,3 +110,10 @@ sw.*

# Resources
+resources
.output
test-results
playwright-report
.nuxtrc

# graphql
*.generated.ts
8 changes: 8 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
// Recommended when opening this workspace. ESLint is required for the
// fix-on-save setup above; Volar powers Vue/TS in .vue files.
"recommendations": [
"dbaeumer.vscode-eslint",
"Vue.volar"
]
}
23 changes: 23 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
// ESLint owns formatting in this project (@nuxt/eslint-config `stylistic`), so we
// don't use a separate formatter/Prettier. Instead of "format on save" we
// run ESLint's autofix on save — this applies stylistic formatting AND any
// other auto-fixable rules in a single pass.
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},

// Don't run a second formatter on top of ESLint (avoids fighting rules).
"editor.formatOnSave": false,

// ESLint flat config lives at ./eslint.config.mjs (default in recent
// versions of the extension, set explicitly to be safe).
"eslint.useFlatConfig": true,

// Lint/fix these languages, including Vue SFCs.
"eslint.validate": [
"javascript",
"typescript",
"vue"
]
}
66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Working agreements

- **Never `git commit` automatically.** Only commit when the user explicitly tells you to in that moment (e.g. "commit this", or invoking `/commit`). Completing a task is not implicit permission, and a prior commit request does not authorize future ones. Make and stage edits, then stop and wait for explicit authorization.
- **Keep commit messages minimal.** Default to just the `<type>(<scope>): <subject>` line — no body. Only add a short, one-line body for genuinely important or non-obvious commits; never write a multi-paragraph explanation.
- **Package manager is Yarn 4** (pinned via `packageManager`); use `yarn`, not npm/pnpm.
- **Prefer `@/` for source-root imports** (e.g. `@/stores/main`, `@/utils/factories`, `@/components/...`). `@` is pinned to the source root in `nuxt.config.ts` (`alias`) and resolves via the generated TS paths. Note there is no `src/` folder — `srcDir` is `.`, so `@` (and the still-valid `~`) map to the repo root.
- **Colocate `.graphql` files with their primary consumers.** Place `.graphql` files in the same directory as the component or page that primarily consumes them, using the same base name (e.g. `components/Footer.vue` + `components/Footer.graphql`, or `pages/aktuality/[slug].vue` + `pages/aktuality/[slug].graphql`). Use the **Czech (canonical) slug** for the directory. After adding/editing a `.graphql` operation, run `yarn codegen` to regenerate types.
- **Do not use Gutenberg blocks.** This project does not author content as Gutenberg blocks (no `<!-- wp:… -->` block markup in `post_content` or ACF wysiwyg fields). Write content as plain HTML with the final Tailwind classes applied directly. Page structure is expressed through the ACF flexible-content **sections** model (`pages/[slug].vue`), and rich text as plain HTML — not the block editor. When reproducing a block-based layout (e.g. the old `render_block`-filtered "O nás" look), copy its **rendered** HTML/classes, not the block source.
- **Colocate component tests with the component (or page) they test.** Place `*.spec.{js,ts}` files in the same directory as the `.vue` file they mount, using the same base name (e.g. `components/Footer.vue` + `components/Footer.spec.ts`, or `pages/newsletter.vue` + `pages/newsletter.spec.ts`). There is no more `tests/component/` — `vitest.config.ts`'s `include` globs `components/**/*.spec.{js,ts}` and `pages/**/*.spec.{js,ts}` directly, matching the existing `utils/**/*.spec.ts` colocation. Pure, no-single-file-subject tests still live in `tests/unit/`.

## What this is

Server-side-rendered (SSR) marketing/events site for Studio Bubec. **Nuxt 4.4 / Vue 3 / Pinia**, run as a long-lived Node server (Nitro `node-server` preset). Content is pulled from a **headless WordPress WPGraphQL API** (`apiGraphqlUrl`, default `https://bubec-wordpress.assemblage.cz/graphql`, overridable via `WP_GRAPHQL_URL`) **on demand** at request time; performance and WP-API load are handled by Nitro route caching (`routeRules` stale-while-revalidate), not a static build. Deployed as a Docker container running `node .output/server/index.mjs`. The WordPress backend lives in the separate **`bubec-wordpress`** repo (see its own `CLAUDE.md`).

> Migrated from Nuxt 2.18 / Vue 2 / Vuex (static `nuxt generate`), and subsequently from the WordPress **REST** API to **WPGraphQL**. There is no `nuxt generate`, no build-time route enumeration, no `dist/` output, and no more axios/REST factories.

## Commands

Package manager is **Yarn 4** (pinned via `packageManager`); use `yarn`, not npm/pnpm. Node 24.

- `yarn dev` — dev server on :3000 (or `docker-compose up --build` for the Docker env)
- `yarn dev:mock` — dev server backed by local `mock/*.json` instead of the live API (`MOCK_DATA=1`)
- `yarn build` — compile the SSR server output into `.output/`
- `yarn start` — run the built server (`node .output/server/index.mjs`) on :3000
- `yarn codegen` — regenerate `models/graphql.generated.ts` from the `.graphql` operations against the live WPGraphQL schema (reads `.env` for `WP_GRAPHQL_URL`)
- `yarn lint` / `yarn lint:fix` — ESLint (standalone flat config built with `@nuxt/eslint-config`, includes stylistic formatting; not generated from `.nuxt/`)
- `yarn typecheck` — `nuxt typecheck` (vue-tsc)
- `yarn test` / `yarn test:watch` — Vitest unit + component tests (`yarn test:unit` / `yarn test:component` run each subset alone)
- `yarn test:e2e` — Playwright e2e against the built server; run `yarn build` first (needs `npx playwright install chromium` once)
- `yarn fetch-mock` — one-off: repopulate `mock/*.json` from the live WP REST API (`scripts/fetch-mock-data.mjs`)

**macOS dev caveat:** the `dev` script is `TMPDIR=/tmp nuxt dev`. Nuxt 4's dev server connects to its vite-node worker over a unix socket in `$TMPDIR`; macOS's long `/var/folders/…` TMPDIR pushes the socket path past the OS 104-char limit (`connect EINVAL`), so a short TMPDIR is required. The `test`/`test:e2e` scripts set it too.

## Architecture

**Data flow (all content is WordPress, via WPGraphQL).** `utils/graphql.ts` exports the single access helper `gqlFetch($config, query, variables)` — a thin `$fetch` POST to `apiGraphqlUrl` that unwraps `data` and throws on GraphQL `errors`. There is no client library and no axios. Queries are authored as **colocated `.graphql` files** imported raw (`import q from "./x.graphql?raw"`) and passed to `gqlFetch`, typically inside `useAsyncData`. Example (`pages/aktuality/index.vue`):

```ts
import { gqlFetch } from "@/utils/graphql";
import newsListQuery from "@/pages/aktuality/index.graphql?raw";
const config = useRuntimeConfig().public;
const { data } = await useAsyncData("newsList",
() => gqlFetch(config, newsListQuery, { language: language.value }),
{ watch: [language] });
```

- **Types are generated, not hand-written.** `graphql-codegen` (`codegen.ts`) reads every `.graphql` under `stores/`, `pages/`, `components/` and emits operation + schema types to `models/graphql.generated.ts` (committed). `models/index.ts` re-exports those plus `models/wp.ts`, so consumers import query result types (`GlobalDataQuery`, etc.) and `WpConfig` from `@/models`. Run `yarn codegen` after changing any operation.
- **Global data** (menus, ACF options, featured posts) is the one store: `stores/main.ts` (Pinia, option-store style) has a single guarded `fetchGlobalData()` action that runs `stores/main.graphql`. `app.vue` calls `store.fetchGlobalData()` inside `useAsyncData("global", …)` for every route (the old `nuxtServerInit`). Per-page/detail data is fetched directly with `gqlFetch` in the page's own `useAsyncData`.
- **Mock mode.** `mock/*.json` holds curated, data-only content; `yarn dev:mock` / `yarn build:mock` set `MOCK_DATA=1` (surfaced as `runtimeConfig.public.mockData`) to serve from it instead of the live API. Regenerate the JSON with `yarn fetch-mock`.
- **No build-time `getRoutes()`/payload.** SSR renders each route on demand. `routeRules` in `nuxt.config.ts` cache rendered HTML with stale-while-revalidate (`/**` 60s, `/newsletter/**` 1h) — this is what keeps WP load low while staying fresh.

**Modules.** `@pinia/nuxt`, `@nuxtjs/i18n`, `@nuxtjs/sitemap` (`@nuxtjs/sitemap` + `site.url`/`trailingSlash` drive `/sitemap.xml`). ESLint is *not* a Nuxt module here — `eslint.config.mjs` builds a standalone flat config via `@nuxt/eslint-config`'s `createConfigForNuxt` (hand-maintained `dirs`), so `yarn lint` works without any `.nuxt/` prepare step.

**i18n & routing.** Bilingual via `@nuxtjs/i18n` **v10**: `cs` (default) and `en`, locale files in `i18n/locales/`, Vue-I18n runtime options in `i18n/i18n.config.ts` (`legacy: false`, `globalInjection: true` so templates keep `$t`/`$i18n`; in `<script setup>` use `useI18n()`). Czech is the canonical slug; English paths are remapped in the `i18n.pages` block of `nuxt.config.ts`. Slug mapping: `aktuality`→`news`, `program`→`events`, `kurz`→`course`. Trailing slashes enforced (`site.trailingSlash: true`). `compilation.strictMessage: false` + `escapeHtml: false` are set because some locale messages contain HTML (e.g. `cookiePolicy.message`), which i18n v10 otherwise rejects/escapes. The GraphQL layer maps the locale to a `language` variable (`CS`/`EN`, via wp-graphql-polylang).

**Pages/components.** `pages/` uses Nuxt file-based routing with `[slug].vue` dynamic detail pages per content type; `pages/kurz/index.vue` and `pages/program/detail/index.vue` redirect to `/` via `definePageMeta({ redirect })`. **All pages and components are `<script setup>` / Composition API** — no Options API, no `this.$config`/Vuex/`mapState`. `components/` are auto-imported (`components: true`). Styling is Tailwind (`tailwind.config.js`, wired through `postcss` + `assets/css/main.css`) plus SCSS variables/mixins auto-injected via Vite `css.preprocessorOptions` (`@use "@/assets/scss/_variables.scss" as *`, replacing `@nuxtjs/style-resources`). Static assets are served from `public/` (footer logos under `public/images/`). Head/SEO uses `useHead` + `useLocaleHead`; `utils/functions.ts` `getOpenGraphMeta` builds OG/Twitter tags (keyed by `key` for dedup).

**Testing.** Vitest + `@nuxt/test-utils` (pinned to **vitest 4**, which the test-utils Nuxt environment requires): `tests/unit/` are pure (node env) tests not tied to one component/page. Component/page tests mount with `mountSuspended` (Nuxt env, happy-dom) and are colocated next to the `.vue` file they test (`components/Footer.spec.ts`, `pages/newsletter.spec.ts`, …) — see the colocation working agreement above. Playwright (`tests/e2e/`) builds the SSR output and drives the real `node .output/server` (`playwright.config.ts` `webServer`).

**`bubec-theme/`** is a WordPress theme copy checked into this repo but **not part of the Nuxt build**. The live headless backend and its canonical theme live in the separate **`bubec-wordpress`** repo.

Runtime config (titles, SEO, per-env base URLs) lives in `project.config.js`; `apiGraphqlUrl` is added in `nuxt.config.ts` (`runtimeConfig.public`). Read it app-wide with `useRuntimeConfig().public` (there is no `this.$config`).
10 changes: 7 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ COPY . .

# Set environment variables to use custom CA if needed
ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV NODE_ENV=production

# Expose Nuxt development port
# Build the SSR server output (Nitro node-server preset)
RUN yarn build

# Expose Nuxt SSR port
EXPOSE 3000

# Default command: Run Nuxt in development mode
CMD ["npm", "run", "dev"]
# Run the Nitro node-server output
CMD ["node", ".output/server/index.mjs"]
19 changes: 19 additions & 0 deletions Dockerfile.deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Deploy-only image: expects a pre-built .output/ in the build context
# (produced by `yarn build` in CI). Does NOT run yarn install/yarn build —
# see Dockerfile for the full-build image used by local/dev Docker.
FROM node:24-slim

WORKDIR /app

RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates

COPY .output ./.output

ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV NODE_ENV=production
ENV NITRO_PORT=3000
ENV NITRO_HOST=0.0.0.0

EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]
Loading
Loading