diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e4532a3b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "i18n-ally.localesPaths": ["i18n", "locales", "i18n/locales"] +} diff --git a/AGENTS.md b/AGENTS.md index c2d00bb9..7596d040 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,37 +5,38 @@ - **Nuxt 4** (SSR, Nitro server) with **Vue 3** + TypeScript - **TailwindCSS v4** via `@tailwindcss/vite` Vite plugin (NOT PostCSS) - **Vitest** + `@nuxt/test-utils` with Playwright browser mode for testing -- **Prettier** (formatting). ESLint config exists but is not wired to any npm script. +- **Prettier** (formatting). ESLint config exists but is not wired to any package script. ## Setup ```bash cp .example.env .env # then edit .env -npm install # triggers nuxt prepare via postinstall +pnpm install # triggers nuxt prepare via postinstall ``` - **Node ≥ 24** required (`package.json` engines) +- **pnpm ≥ 11.1** required (`packageManager` pins the expected version) - **Git submodule** at `assets/lib/rule-34-shared-resources` — clone with `--recursive` - **External API**: the app calls a separate API service at `NUXT_PUBLIC_API_URL` (default `http://localhost:8081`). The API codebase is at [github.com/Rule-34/API](https://github.com/Rule-34/API). ## Commands -| Command | What it does | -|----------------------|-----------------------------------------------| -| `npm run dev` | Dev server at `localhost:8080` | -| `npm run build` | Production build into `.output/` | -| `npm run generate` | Static generation | -| `npm test` | `vitest run` | -| `npm run test:watch` | `vitest watch` | -| `npm run release` | `standard-version` for versioning + changelog | +| Command | What it does | +| ----------------- | --------------------------------------------- | +| `pnpm dev` | Dev server at `localhost:8080` | +| `pnpm build` | Production build into `.output/` | +| `pnpm generate` | Static generation | +| `pnpm test` | `vitest run` | +| `pnpm test:watch` | `vitest watch` | +| `pnpm release` | `standard-version` for versioning + changelog | ## Architecture Single Nuxt app. Key directories: | Dir | Purpose | -|----------------------|-------------------------------------------------------------------------------------------| +| -------------------- | ----------------------------------------------------------------------------------------- | | `config/` | Centralized project config (`project.ts` for branding/URLs, `i18n.ts` for locales) | | `app/` | Nuxt app-level config (router options, SPA loading template) | | `composables/` | Shared Vue composables (auto-imported by Nuxt) | @@ -63,8 +64,8 @@ them as `` not ``. `mirroredRouteRules()` helper so prefixed paths get the same caching/SSR rules. - **Known bug**: `canonicalQueries` in the i18n module config is a no-op in v10. A two-part workaround is required: 1. SSR: `server/plugins/fix-canonical-queries.ts` patches the canonical `` in rendered HTML. - 2. CSR: `pages/posts/[domain].vue` uses `useHead` to re-apply the canonical after i18n overwrites it on hydration. - See the removal checklist in `fix-canonical-queries.ts` for when upstream fixes this. + 2. CSR: `pages/posts/[domain]/index.vue` uses `useHead` to re-apply the canonical after i18n overwrites it on hydration. + See the removal checklist in `fix-canonical-queries.ts` for when upstream fixes this. ### SEO & Head Management @@ -75,11 +76,16 @@ them as `` not ``. `useRequestURL().origin` on the server only (`app.vue`). i18n does not touch `og:image` during hydration. - **Canonical URLs must point to production** (`https://r34.app/…`) even when served from clone domains. This is intentional for SEO — canonicals prevent duplicate content. Use `project.urls.production` for canonicals. +- **Schema.org breadcrumb item URLs should stay local/locale-relative**. Do not convert breadcrumb items to + `project.urls.production`; production-absolute URLs are for canonicals. - **Page-specific tags** (title, description) should use `useSeoMeta` in the page component. ### Router - Custom scroll behavior: skips scroll-to-top when only the `page` query param changes between same-route navigations. +- Query filters intentionally use flat bracket keys (`filter[sort]`, `filter[rating]`, etc.) with Vue Router's default + query handling. Do not re-add `qs` for nested `route.query.filter` objects unless the URL contract changes; `qs` puts + a measurable parser/stringifier cost on the first-load router path. - Legacy redirect: `server/middleware/redirect-to-posts.get.ts` redirects `/?domain=x&page=…&tags=…` → `/posts/x?page=…&tags=…` (301). @@ -88,6 +94,35 @@ them as `` not ``. A custom `imgproxy` provider is registered for `` (see `nuxt.config.js` → `image.providers`). Images are deliberately generated at 1x density only (webp format) to reduce bandwidth. +- `@nuxt/image` v2 supports `preload: { fetchPriority: 'high' }`. Use the module API for image preload priority instead + of patching rendered HTML in Nitro. +- `PostMedia` uses imgproxy for SSR post images, including local development. Non-premium SPA navigations keep the direct + image path; validate image delivery in an environment where imgproxy can resolve the source URL. + +### Headless UI + +- Do not add `provideHeadlessUseId` in `app.vue` while the project uses Vue 3.5+ and `@headlessui/vue` 1.7.23+; those + versions use Vue's native `useId` and the Nuxt Headless UI workaround is only for older versions. + +### Performance + +- Prefer high-impact, measurable optimizations over small rewrites. Keep battle-tested dependencies unless replacing one + has a clear, measured payoff. +- After substantial performance changes, verify with a production build, relevant tests, request traces, and Lighthouse + against the built app before deciding the change is worth keeping. +- Production is behind Cloudflare, which Brotli-compresses HTML responses. Do not add app-level HTML compression unless + a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse. +- Keep the global TanStack Vue Query plugin unless a larger measured payoff appears. A route-scoped `QueryClient` + experiment on 2026-05-17 saved only about 8 KB compressed on the homepage and did not move Lighthouse, while adding + custom SSR hydration logic. +- Keep `features.inlineStyles: false` unless new measurements justify revisiting it. Enabling it on 2026-05-17 doubled + homepage HTML from about 51 KB to 106 KB, increased Lighthouse byte weight from 361 KiB to 406 KiB, and did not improve + the performance score. +- Keep `@formkit/auto-animate` route-scoped unless it is used broadly. The Nuxt module registers a global directive and + puts the runtime in the first-load entry; local `vAutoAnimate` imports on the premium CSR pages saved about 3 KB gzip. +- For URL validation/parsing, prefer `URL.canParse()` or `URL.parse()` over constructor `try/catch`; use `URL.parse()` + when the parsed URL object is needed, with a `URL.canParse()` fallback in browser code if compatibility matters. + ### PWA The service worker is intentionally disabled (`selfDestroying: true`). Do not add service worker logic. @@ -116,12 +151,12 @@ Tailwind v4 uses CSS-based config (`assets/css/main.css`), NOT PostCSS. The `tai ### Docker production build -- Multi-stage: build stage needs `SENTRY_*` args for source map uploads; production stage copies only `.output/` (no - `node_modules` needed — Nitro bundles everything). +- Multi-stage: build stage uses `pnpm install --frozen-lockfile`. Source map uploads need `SENTRY_ORG`, + `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN`; set `SENTRY_UPLOAD_SOURCE_MAPS=false` to skip them. The production stage + copies only `.output/` (no `node_modules` needed — Nitro bundles everything). - `NITRO_PRESET` build arg selects the deployment target. ### Prettier Key settings: 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates. - diff --git a/Dockerfile b/Dockerfile index 928526c3..0ed08fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,21 +7,23 @@ ARG NITRO_PRESET ARG SENTRY_ORG ARG SENTRY_PROJECT ARG SENTRY_AUTH_TOKEN +ARG SENTRY_UPLOAD_SOURCE_MAPS=true ENV NITRO_PRESET=${NITRO_PRESET} \ SENTRY_ORG=${SENTRY_ORG} \ SENTRY_PROJECT=${SENTRY_PROJECT} \ - SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} + SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ + SENTRY_UPLOAD_SOURCE_MAPS=${SENTRY_UPLOAD_SOURCE_MAPS} WORKDIR /app -COPY package.json package-lock.json ./ +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -RUN npm ci +RUN corepack enable && pnpm install --frozen-lockfile COPY . . -RUN npm run build +RUN pnpm run build # Stage 2: Production FROM node:${NODE_VERSION}-alpine AS production diff --git a/README.md b/README.md index ae43d598..d7b1e6c3 100644 --- a/README.md +++ b/README.md @@ -100,16 +100,16 @@ cp .example.env .env ```bash # Install dependencies -npm install +pnpm install # Serve with hot reload at localhost:8080 -npm run dev +pnpm dev # Build for production -npm run build +pnpm build # Generate static project -npm run generate +pnpm generate ``` For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). diff --git a/app.vue b/app.vue index 2b32fd3b..a8d57680 100644 --- a/app.vue +++ b/app.vue @@ -1,9 +1,6 @@ + + diff --git a/components/layout/DialogManager.vue b/components/layout/DialogManager.vue index 112ba005..4271dd8f 100644 --- a/components/layout/DialogManager.vue +++ b/components/layout/DialogManager.vue @@ -1,162 +1,80 @@