From 84c2d22236ec5a225fcbb6ac7811f9da21cd22ea Mon Sep 17 00:00:00 2001 From: Alejandro Akbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Wed, 20 May 2026 10:29:31 +0200 Subject: [PATCH 1/6] perf: optimize routing, media, and SEO --- .vscode/settings.json | 3 + AGENTS.md | 65 +- Dockerfile | 10 +- README.md | 8 +- app.vue | 3 - app/router.options.ts | 51 +- assets/js/RouterHelper.ts | 26 +- assets/js/nuxt-image/imgproxy.provider.ts | 4 +- assets/js/sidebarLinks.ts | 52 +- components/layout/ClientToaster.vue | 13 + components/layout/DialogManager.vue | 184 +- components/layout/modal/ReviewPrompt.vue | 4 +- components/layout/navigation/Navbar.vue | 40 +- components/layout/navigation/Sidebar.vue | 15 +- .../layout/navigation/SidebarWrapper.vue | 180 +- components/pages/home/FeaturedTags.vue | 57 +- components/pages/home/Newsletter.vue | 2 +- components/pages/home/PageHistory.vue | 3 +- components/pages/home/PageHistorySection.vue | 16 + components/pages/posts/PostsPageFooter.vue | 14 +- .../pages/posts/navigation/DomainSelector.vue | 15 +- .../navigation/DomainSelectorFallback.vue | 8 +- .../navigation/search/TagCollections.vue | 21 +- .../posts/post/PostChatWithAiFallback.vue | 20 + components/pages/posts/post/PostComponent.vue | 212 +- .../pages/posts/post/PostDownloadFallback.vue | 18 + components/pages/posts/post/PostMedia.vue | 192 +- .../pages/posts/post/PostSaveFallback.vue | 9 +- .../pages/posts/post/PostShareFallback.vue | 18 + components/pages/posts/post/PostSource.vue | 37 +- .../pages/posts/post/PostSourceFallback.vue | 18 + components/pages/posts/post/PostTag.vue | 32 +- .../posts/post/PostTagsToggleFallback.vue | 19 + components/shared/ContentContainer.vue | 16 +- components/shared/ShareButton.vue | 3 +- composables/navigation/useMenu.ts | 2 +- composables/useAdvertisements.ts | 9 + composables/useDialogManagerState.ts | 65 + composables/useFaviconUrl.ts | 7 + composables/useIdleTask.ts | 16 + composables/useInteractionDetector.ts | 8 +- composables/useLazyToast.ts | 53 + config/i18n.ts | 5 +- i18n/locales/de.json | 539 + i18n/locales/en.json | 4 +- i18n/locales/es.json | 4 +- i18n/locales/fr.json | 539 + i18n/locales/ja.json | 4 +- i18n/locales/pt.json | 539 + i18n/locales/ru.json | 4 +- layouts/default.vue | 32 +- nuxt.config.js | 127 +- package-lock.json | 27435 ---------------- package.json | 6 +- pages/cookie-policy.vue | 11 + pages/dmca.vue | 18 +- pages/index.vue | 92 +- pages/legal.vue | 10 + pages/other-sites.vue | 10 + pages/posts/[domain].vue | 213 +- pages/premium/additional-boorus.vue | 61 +- pages/premium/backup.vue | 15 +- pages/premium/dashboard.vue | 24 +- pages/premium/forgot-password.vue | 12 +- pages/premium/index.vue | 25 +- pages/premium/saved-posts/[domain].vue | 40 +- pages/premium/sign-in.vue | 12 +- pages/premium/tag-collections.vue | 50 +- pages/privacy-policy.vue | 10 + pages/settings.vue | 12 +- pages/tags/[domain]/[tag].vue | 328 + pages/terms-of-service.vue | 11 + plugins/035.sentry.client.ts | 66 +- plugins/040.matomo.client.ts | 4 +- plugins/050.formbricks.client.ts | 6 +- pnpm-lock.yaml | 17621 ++++++++++ pnpm-workspace.yaml | 6 + public/llms.txt | 27 + server/api/_sitemap-urls.ts | 8 +- server/plugins/lcp-preload-fetchpriority.ts | 39 - test/assets/router-helper.test.ts | 36 +- test/helper.ts | 26 +- test/pages/posts.test.ts | 332 +- test/pages/premium/backup.test.ts | 84 - test/router.options.test.ts | 23 + test/server-mocks/plugin.ts | 47 +- test/server/fix-canonical-queries.test.ts | 15 +- test/server/path.test.ts | 2 +- 88 files changed, 21597 insertions(+), 28485 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 components/layout/ClientToaster.vue create mode 100644 components/pages/home/PageHistorySection.vue create mode 100644 components/pages/posts/post/PostChatWithAiFallback.vue create mode 100644 components/pages/posts/post/PostDownloadFallback.vue create mode 100644 components/pages/posts/post/PostShareFallback.vue create mode 100644 components/pages/posts/post/PostSourceFallback.vue create mode 100644 components/pages/posts/post/PostTagsToggleFallback.vue create mode 100644 composables/useDialogManagerState.ts create mode 100644 composables/useFaviconUrl.ts create mode 100644 composables/useIdleTask.ts create mode 100644 composables/useLazyToast.ts create mode 100644 i18n/locales/de.json create mode 100644 i18n/locales/fr.json create mode 100644 i18n/locales/pt.json delete mode 100644 package-lock.json create mode 100644 pages/tags/[domain]/[tag].vue create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 public/llms.txt delete mode 100644 server/plugins/lcp-preload-fetchpriority.ts delete mode 100644 test/pages/premium/backup.test.ts create mode 100644 test/router.options.test.ts 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..be95557a 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) | @@ -64,7 +65,7 @@ them as `` not ``. - **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. + 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 @@