Skip to content

feat: localization#120

Open
AlejandroAkbal wants to merge 33 commits intomainfrom
i18n
Open

feat: localization#120
AlejandroAkbal wants to merge 33 commits intomainfrom
i18n

Conversation

@AlejandroAkbal
Copy link
Copy Markdown
Member

@AlejandroAkbal AlejandroAkbal commented May 4, 2026

Summary by CodeRabbit

  • New Features

    • Full i18n rollout (EN/ES/JA/RU), locale-aware routing, and new locale/tag helpers.
  • Improvements

    • Extensive UI localization across pages/components, localized SEO/head text, accessible labels, locale-aware links, promotions FAQ/pricing localization, and config refinements.
  • Bug Fixes

    • Canonical/tag query handling hardened, sitemap resilience improved, and LCP/preload behavior refined.
  • Documentation

    • Added comprehensive AGENTS.md with setup, architecture, testing, and Docker notes.
  • Tests

    • Updated and added locale-aware tests, mocks, and test plugins.

Wrap the console.info calls in an `if (import.meta.client)` check so the messages only run in the browser (avoids server-side logging during SSR). Also apply minor whitespace/formatting adjustments to imports and the comment; lazy-loading of vue-sonner CSS remains unchanged.
Introduce internationalization and locale-aware routing across the app. Adds config/i18n.ts and new composable locale.ts (and removes the old useLocalizedHref), registers @nuxtjs/i18n in nuxt.config.js with mirrored route rules and server-side SEO/head adjustments. Localizes multiple components (app.vue, FeaturedTags, PromotedContent, PostSource, PromotionalBanner, premium pages) to use useI18n, localePath and isExternalHref, refactors static strings into translation keys, and converts several data structures to computed for reactive translations. Also adds server plugins (fix-canonical-queries, lcp-preload-fetchpriority) and updates locale JSON files.
Wrap the Matomo API call in a try/catch and initialize popularSiteSearchKeywords to an empty array. On failure it now logs a warning and continues, so sitemap generation won't fail and will simply exclude dynamic tag URLs when the Matomo request errors. Adds a MatomoResponse[] type annotation for the keyword list.
Introduce AGENTS.md to centralize developer onboarding and project conventions. The document outlines the stack (Nuxt 4, Vue 3, TypeScript), setup steps, npm scripts, project architecture, and important conventions/gotchas (component auto-imports, i18n canonical query workaround, router/query handling, custom image provider, PWA disabled, Tailwind v4 usage, Sentry config, testing setup, and Docker production build notes). This provides a single reference for contributors to install, run, and follow project-specific patterns.
Add Vitest config and tests to validate canonical URL behavior and hreflang alternates, plus a Nitro test plugin to mock booru API endpoints so SSR tests don't hit the real API. vitest.config.ts disables Sentry uploads during tests and raises the test timeout. test/server-mocks/plugin.ts registers a simple /booru handler returning mock data; test/server/fix-canonical-queries.test.ts covers canonical query handling (tags, encoding, stripping non-canonical params) and alternate links across locales.
Add a test-only auth bypass and related test updates: introduce public.testAuthBypass runtime flag (exposed in $test) and set VITE_TEST_AUTH_BYPASS in vitest config so middleware can skip redirects during tests. Update middleware/auth to early-return when testAuthBypass is enabled. Standardize tag API calls to use $fetch with baseURL (use config.public.apiUrl) in pages/index.vue and pages/posts/[domain].vue. Adjust test setup (browserOptions rename) and enhance premium/backup test to use url(), capture page errors, instrument Date.toLocaleString to verify backup behavior, and assert stability. Remove legacy migrate-old-data test and its mock data. Also remove one auth server-mocks plugin from the $test nitro plugins.
Add logic to append encoded ?tags=... to canonical link hrefs for posts pages: extract patch into patchCanonicalTags, only run when tags param present and when pathname matches /posts, and update response body accordingly. Refactor posts.test.ts to introduce mockBooruApi helper, switch route matchers to booru endpoints, use domcontentloaded waits, more deterministic selectors/waits, replace fragile list-count assertions with explicit test-id checks, and convert/sketch some skipped tests to todo for improved stability. Add server tests for clone-domain canonicalization (ensure canonical points to production origin) and import project config for assertions.
Switch hardcoded UI strings to i18n and refactor tag/title utilities and page history handling.

Changes include:
- Replace many hardcoded strings in PostsPageFooter and PostComponent with $t(...) translations and introduce typed SelectedFilters.
- Add useTagTitle composable to build locale-aware tag titles; remove tagArrayToTitle logic from SeoHelper.
- Localize Navbar labels/links (use localePath, getRouteBaseName) and simplify intersection observer logic.
- Improve PostComponent: typed tag handling, createTag helper using TagDTO, localized alt/title/share text, and compute postSourceFileUrl.
- Refactor usePageHistory: use router/getRouteBaseName, compare URLs ignoring only the page query param, guard server/client via import.meta.server, and keep history deduplicated/sorted.

These changes improve localization, type safety, and URL handling for posts/pages.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@AlejandroAkbal has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 44 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 45af961b-f56f-4866-84d8-2140f59066a1

📥 Commits

Reviewing files that changed from the base of the PR and between e63eeca and 3b8c892.

📒 Files selected for processing (1)
  • test/pages/posts.test.ts
📝 Walkthrough

Walkthrough

Adds full i18n (config + en/es/ja/ru), localizes ~50+ components/pages/middleware to use translations and locale-aware routing, introduces locale helpers/composables, adds two Nitro render plugins for canonical query fixes and LCP preload fetchpriority, hardens sitemap, updates test tooling/mocks, and adds AGENTS.md docs.

Changes

I18n, Routing, SSR & Tests (single cohort)

Layer / File(s) Summary
Data / Config
config/i18n.ts, i18n/locales/en.json, i18n/locales/es.json, i18n/locales/ja.json, i18n/locales/ru.json, config/promotions.ts, package.json
Adds central i18n config and four locale files; converts promotion UI strings to translation keys; adds @nuxtjs/i18n and updates dependencies/devDependencies.
Core Runtime & Build
nuxt.config.js, app.vue, AGENTS.md
Enables Nuxt i18n module and locale-aware routeRules; refactors app-level head/SEO to use translations; adds AGENTS.md documenting stack, setup, and conventions.
Composables / Helpers
composables/locale.ts, composables/useTagTitle.ts, composables/usePageHistory.ts, composables/useTagCollections.ts, assets/js/SeoHelper.ts
Adds stripLocaleFromPath and isExternalHref; introduces useTagTitle for building tag-based titles; makes page-history and tag-collections i18n-aware; removes tagArrayToTitle.
SSR Plugins / Sitemap
server/plugins/fix-canonical-queries.ts, server/plugins/lcp-preload-fetchpriority.ts, server/api/_sitemap-urls.ts
Adds Nitro render plugin to append ?tags= to canonical links for posts; normalizes LCP preload patching using stripped locale paths and marks preloads with fetchpriority="high"; sitemap handler wrapped to tolerate Matomo failures and use _i18nTransform.
Wiring: Components, Pages & Middleware
assets/js/sidebarLinks.ts, components/**, components/pages/**, pages/**, middleware/*.ts
Replaces literal UI/ARIA/SEO strings with $t()/t() across ~50+ components/pages; sidebar entries switched to nameKey; internal links use localePath(); middleware redirects use locale-aware targets; small prop typing refinement in PostsPageFooter.vue.
Tests, Mocks & Tooling
package.json, nuxt.config.js ($test), jest.config.js, test/server-mocks/plugin.ts, test/helper.ts, test/pages/*.test.ts, test/server/fix-canonical-queries.test.ts
Moves tests toward Vitest/Playwright via @nuxt/test-utils, removes Jest preset, registers a test-only Nitro plugin for /booru mocks, adjusts browser options, refactors E2E tests to use shared booru mocks, removes obsolete migration tests, and adds SSR canonical URL tests.
Documentation
AGENTS.md
Adds comprehensive project documentation: stack, i18n/testing conventions, Nitro test mocks, Docker/build notes, Tailwind v4 and Prettier rules.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client (Browser)
    participant Nuxt as Nuxt SSR
    participant Nitro as Nitro (render:response)
    participant Matomo as External API (Matomo)

    Browser->>Nuxt: GET /posts/<domain>?tags=...
    Nuxt->>Nitro: render request (render:response hooks)
    Nitro->>Nitro: stripLocaleFromPath(event.path)
    Nitro->>Nitro: lcp-preload-fetchpriority — add fetchpriority to image preloads
    Nitro->>Nitro: fix-canonical-queries — append ?tags=<encoded> to canonical
    Nuxt->>Matomo: fetch popular keywords (optional)
    Matomo-->>Nuxt: keywords or error (handled gracefully)
    Nuxt-->>Browser: SSR HTML (patched head, canonical, preloads)
Loading

Possibly related PRs

  • Rule-34/App#71 — touches PostsPageFooter; related to footer/tag formatting and prop changes.
  • Rule-34/App#79 — overlaps on sidebar link wiring and URL centralization; related to assets/js/sidebarLinks.ts.
  • Rule-34/App#118 — overlaps on post media / AI UI changes in PostComponent/PostChatWithAi.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch i18n
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch i18n

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
components/pages/posts/post/PostTag.vue (1)

48-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

t parameter in filter() callback shadows the useI18n() t function.

The const { t } = useI18n() added on line 48 is shadowed by the arrow function parameter t in the filter((t) => t !== tag.name) call on line 65. While functionally harmless today, it creates a confusing naming collision that could cause a silent bug if someone adds an i18n call inside this callback.

🐛 Proposed fix — rename the filter parameter
-      customBlockList.value = customBlockList.value.filter((t) => t !== tag.name)
+      customBlockList.value = customBlockList.value.filter((blockedTag) => blockedTag !== tag.name)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/post/PostTag.vue` around lines 48 - 65, The i18n
function destructured as t from useI18n() is being shadowed by the filter
callback parameter in toggleBlockedTag; update the filter call on
customBlockList.value.filter((t) => t !== tag.name) (inside the toggleBlockedTag
function) to use a non-conflicting parameter name like item or name (e.g.,
filter((name) => name !== tag.name)) so the i18n t remains unshadowed and the
comparison logic still excludes the tag.name correctly.
components/pages/posts/navigation/search/SearchSelect.vue (1)

43-62: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

LGTM — correct $t() usage for accessibility labels.

Note: the { label } interpolation in $t('search.changeFilter', { label }) will always embed an English word ('Sort', 'Rating', 'Score') because the parent's filterConfig labels are hardcoded in English. Consider i18n-ing the filterConfig option labels in the parent page or accepting a translation key as a prop if full localization of filter label accessibility text is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/navigation/search/SearchSelect.vue` around lines 43 -
62, SearchSelect uses modelValue.label (hardcoded English) inside the $t
interpolation which prevents localized accessibility text; update the parent
that builds filterConfig to provide translated labels (call $t when constructing
each filterConfig.label) or change SearchSelect to accept a labelKey prop (e.g.,
labelKey) instead of raw label and use $t(labelKey) where modelValue.label is
currently used; ensure the prop API and usages in the parent page are updated
consistently (replace references to modelValue.label with $t(labelKey) or pass
translated strings from parent) so the accessibility string
$t('search.changeFilter', { label }) receives a localized label.
components/layout/navigation/Navbar.vue (1)

53-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the matching label for the menu toggle state.

When isMenuActive is true, the button shows the close icon but still announces “Open main menu”. That gives screen-reader users the wrong action.

Suggested fix
-            :aria-label="$t('common.openMenu')"
+            :aria-label="isMenuActive ? $t('common.closeMenu') : $t('common.openMenu')"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/layout/navigation/Navbar.vue` around lines 53 - 67, The aria-label
on the menu toggle is static and always uses $t('common.openMenu') even when
isMenuActive is true; update the button's aria-label to reflect state (use a
ternary or computed property) so it reads $t('common.openMenu') when
isMenuActive is false and $t('common.closeMenu') when isMenuActive is
true—modify the button element that calls toggleMenu() and references
isMenuActive to use the dynamic label (or add a computed like menuAriaLabel that
returns the appropriate translation).
test/pages/premium/backup.test.ts (1)

1-7: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep the browser-test harness explicit in this spec.

This file uses createPage, but it doesn't import debugBrowserOptions, and the setup call hides the browser: true requirement inside shared config. Please wire both into this file so the Playwright setup stays obvious and consistent.

Suggested fix
 import { describe, expect, it } from 'vitest'
 import { createPage, setup, url } from '@nuxt/test-utils'
-import { defaultSetupConfig } from '../../helper'
+import { debugBrowserOptions, defaultSetupConfig } from '../../helper'
 import path from 'path'

 describe('/premium/backup', async () => {
-  await setup(defaultSetupConfig)
+  await setup({
+    ...defaultSetupConfig,
+    browser: true,
+    ...debugBrowserOptions
+  })

As per coding guidelines, **/*.{test,spec}.{ts,tsx} should "Use @nuxt/test-utils with Playwright by calling await setup({ browser: true }) within describe blocks" and "Import debugBrowserOptions from test/helper.ts in test files to enable headful playback with slowMo for debugging".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/premium/backup.test.ts` around lines 1 - 7, The test currently
relies on createPage and setup but doesn't explicitly enable Playwright or
import debugBrowserOptions; update the spec to import debugBrowserOptions
(alongside defaultSetupConfig) and call await setup with an explicit browser:
true and the debugBrowserOptions merged into the config (e.g., await setup({
...defaultSetupConfig, browser: true, browserOptions: debugBrowserOptions })) so
the Playwright harness is obvious and uses headful slowMo for debugging; ensure
references to createPage and url remain unchanged.
pages/index.vue (1)

78-110: ⚠️ Potential issue | 🟠 Major

Handle non-FetchError failures before reading response.data.

The .catch() block returns any thrown error, but only the FetchError branch returns early. A non-FetchError exception (e.g., TypeError, network error) will skip the error handling and fall through to response.data, which attempts to read a .data property that doesn't exist on plain Error objects. This leaves the autocomplete in an invalid state. Restructure with try/catch to handle all errors uniformly and clear stale results on failure.

Proposed change
-    const response = await $fetch(`/booru/${selectedBooru.value.type.type}/tags`, {
-      baseURL: config.public.apiUrl,
-
-      params: {
-        baseEndpoint: selectedBooru.value.domain,
-
-        tag,
-        order: 'count',
-        limit: 20,
-
-        // Booru options
-        httpScheme: selectedBooru.value.config?.options?.HTTPScheme ?? undefined
-      }
-    })
-      //
-      .catch(async (error) => {
-        const Sentry = await import('@sentry/nuxt')
-
-        Sentry.captureException(error)
-
-        return error
-      })
-
-    if (response instanceof FetchError) {
-      switch (response.status) {
+    try {
+      const response = await $fetch(`/booru/${selectedBooru.value.type.type}/tags`, {
+        baseURL: config.public.apiUrl,
+        params: {
+          baseEndpoint: selectedBooru.value.domain,
+          tag,
+          order: 'count',
+          limit: 20,
+          httpScheme: selectedBooru.value.config?.options?.HTTPScheme ?? undefined
+        }
+      })
+
+      searchTagResults.value = response.data
+    } catch (error) {
+      searchTagResults.value = []
+
+      const Sentry = await import('@sentry/nuxt')
+      Sentry.captureException(error)
+
+      if (error instanceof FetchError) {
+        switch (error.status) {
           case 404:
             toast.error(t('toasts.noTagsFound', { tag }))
             break
@@ -92,7 +100,7 @@
           default:
-          toast.error(t('toasts.failedToLoadTags', { message: response.message }))
+          toast.error(t('toasts.failedToLoadTags', { message: error.message }))
           break
-      }
-
-      return
+        }
+
+        return
+      }
+
+      toast.error(t('toasts.failedToLoadTags', { message: error instanceof Error ? error.message : 'Unknown error' }))
     }
-
-    searchTagResults.value = response.data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/index.vue` around lines 78 - 110, The current flow returns errors from
the fetch .catch but only handles FetchError specially, so non-FetchError
exceptions fall through and you attempt to read response.data; refactor the
fetch call into an explicit try/catch around the await so all exceptions are
handled uniformly: in the catch block await import('@sentry/nuxt') and call
Sentry.captureException(error), clear searchTagResults.value (to avoid stale
UI), and then if error is an instance of FetchError run the existing switch/
toast logic (handling 404/429/default) and return early; for any other Error
show a generic toast (or reuse the default branch) and return so code after the
fetch (the assignment to searchTagResults.value = response.data) only runs on a
successful response. Ensure you reference response, FetchError,
searchTagResults.value, and the Sentry capture flow when making the change.
components/pages/posts/PostsPageFooter.vue (1)

105-130: ⚠️ Potential issue | 🟠 Major

Preserve locale in footer navigation links.

The NuxtLink components at lines 110 and 130 use hardcoded paths that drop the active locale. From a localized route like /es/posts/..., clicking related tags or sources navigates to the default-locale route instead of maintaining the current locale context.

Proposed change
   const { t } = useI18n()
+  const localePath = useLocalePath()
@@
             <NuxtLink
               v-for="(tag, index) in RELATED_TAGS"
               :key="tag"
               :title="$t('seoFooter.relatedTagTitle', { tag: formattedRelatedTags[index] })"
-              :to="`/posts/${selectedBooru.domain}?tags=${encodeURIComponent(tag)}`"
+              :to="localePath(`/posts/${selectedBooru.domain}?tags=${encodeURIComponent(tag)}`)"
               class="text-primary text-sm hover:underline"
             >
@@
                 <NuxtLink
                   :title="$t('seoFooter.sourceLinkTitle', { tags: formattedTagsString, domain: booru.domain })"
-                  :to="`/posts/${booru.domain}`"
+                  :to="localePath(`/posts/${booru.domain}`)"
                 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/PostsPageFooter.vue` around lines 105 - 130, The
NuxtLink paths for RELATED_TAGS and relatedBoorus lose the current locale
because they build raw string URLs; update both NuxtLink :to bindings (the ones
using selectedBooru.domain and booru.domain) to be route objects that include
the current locale (e.g., :to="{ path: `/posts/${selectedBooru.domain}`, query:
{ tags: tag }, locale: $i18n.locale }" for the RELATED_TAGS loop and :to="{
path: `/posts/${booru.domain}`, locale: $i18n.locale }" for the relatedBoorus
loop) or alternately use route names with params and locale — reference
RELATED_TAGS, formattedRelatedTags, selectedBooru.domain, relatedBoorus,
formattedTagsString and replace the string-template :to values with locale-aware
route objects so clicks preserve the active locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Around line 53-92: The MD022 lint errors are caused by subsection headings
(e.g., "### Component auto-imports", "### i18n", "### Router", "### Images",
"### PWA", "### TailwindCSS", "### Sentry", "### Testing", "### Docker
production build", "### Prettier") having their body text immediately follow the
heading; edit AGENTS.md and insert a single blank line after each of those "###"
headings so the heading is followed by an empty line before the paragraph
content to satisfy markdownlint.

In `@components/layout/navigation/Sidebar.vue`:
- Around line 46-49: The NuxtLink currently uses :href="localePath(item.href)"
for all items which wrongly transforms external URLs; change the href binding to
use item.href directly when item.isExternal is true (e.g., conditional
expression using localePath(item.href) only when !item.isExternal) and add
rel="noopener noreferrer" whenever :target="item.isExternal ? '_blank' :
undefined" is set; update the NuxtLink element (referencing NuxtLink,
localePath, and item.isExternal) so external sidebar links bypass localePath and
include the required security rel attributes.

In `@components/pages/home/FeaturedTags.vue`:
- Around line 86-88: The href currently always calls localePath(tag.path) which
will wrongly prefix locales for external URLs and the rel/target bindings use
empty strings causing dangling attributes; update the template in
FeaturedTags.vue to compute href and attributes using isExternalHref(tag.path):
set :href="isExternalHref(tag.path) ? tag.path : localePath(tag.path)" and
change :rel and :target to return null for internal links (e.g.
:rel="isExternalHref(tag.path) ? 'noopener noreferrer nofollow' : null" and
:target="isExternalHref(tag.path) ? '_blank' : null") so external links keep the
original URL and internal links get localized paths while avoiding empty
attributes.

In `@components/pages/posts/post/PostSave.vue`:
- Line 70: Update the aria-label to reflect the current action by switching from
a static $t('common.savePost') to a conditional based on the component state
(e.g., use isPostSaved to choose between $t('common.unsavePost') and
$t('common.savePost')); add the missing locale key common.unsavePost to the
locale files if it doesn't exist so screen readers announce the correct action
when calling the remove/save toggle in PostSave.vue.

In `@components/pages/posts/PostsPageFooter.vue`:
- Around line 2-5: The template references project.shortName but the script no
longer provides project; fix by re-introducing project into the component's
script (either as a prop or a computed from your store) so the template can
access it. Specifically, add a prop named project: { type: Object as
PropType<Domain>, required: true } to the PostsPageFooter component's props (or
create a computed getter that returns the Domain from your store and expose it
under the name project) so project.shortName in the template resolves correctly.

In `@components/pages/posts/PromotedContent.vue`:
- Around line 79-82: The href currently always calls localePath(promo.link)
which breaks for absolute external URLs; change the binding so it only uses
localePath when the link is internal by checking isExternalHref(promo.link)
first and otherwise using promo.link directly (e.g.
:href="isExternalHref(promo.link) ? promo.link : localePath(promo.link)");
update the component places that render promotions (references to promo.link in
otherPromotions/referralPromotions rendering) so they all use the same
conditional logic and keep existing target/rel logic that already uses
isExternalHref().

In `@components/shared/PromotionalBanner.vue`:
- Line 56: Move the call to useI18n() to the top of the setup block so the
destructured t is available before any computed/getter that references it (e.g.,
countdownText = computed(...)). Specifically, locate the useI18n() call and the
const { t } = useI18n() declaration and hoist that line above other setup logic
(above countdownText and any other computed or functions that call t) to avoid
potential TDZ/ordering issues.

In `@composables/usePageHistory.ts`:
- Around line 19-29: Replace direct URLSearchParams usage in
getComparableUrlWithoutPage: instead of calling url.searchParams.delete(...) and
constructing new URLSearchParams(Array.from(...).sort(...)), parse the query
with qs.parse(url.search || '') to remove the page key, then use
qs.stringify(parsedQuery, { sort: (a,b)=>a.localeCompare(b) }) (or
qs.stringify(parsedQuery, { sort: true }) if preferred) to build the sorted
query string; return `${url.pathname}?${stringified}` only when non-empty. This
keeps URL parsing via new URL(...) for pathname but delegates all query
manipulation to qs per the project's guideline.

In `@composables/useTagTitle.ts`:
- Around line 21-43: The buildTitle function can return an empty string when all
tag names normalize to null; update buildTitle in useTagTitle.ts to return null
if cleanedTags (the array produced by mapping tag.name through
normalizeStringForTitle) is empty before assembling positive/negative parts.
Specifically, after computing cleanedTags (and before using positive/negative),
add a guard that returns null when cleanedTags.length === 0 so callers receive
null instead of ''. Keep the rest of the logic (positive/negative filtering and
using t('seoHelper.without')) unchanged.

In `@i18n/locales/en.json`:
- Line 100: Several i18n JSON values (e.g., the "backupRestored" entry) include
an unnecessary backslash before the exclamation ("\\!"); remove the backslash so
the value reads "Backup restored successfully!" instead of rendering a literal
backslash. Search for any other keys in this file that contain "\\!" (the
comment noted occurrences around lines 482–487 and 497) and update those string
values by deleting the backslash escape, leaving the plain exclamation mark.
- Line 89: Update the shipped typos in the English locale values: replace "Cant"
with "Can't" and "dont" with "don't" in the en.json entries (for example the
value for the cantFindPlatform key should be "Can't find the platform where you
purchased your subscription"); search the file for any other occurrences (note
reviewer called out another occurrence around line 114) and fix them similarly,
ensuring the JSON strings remain valid (properly escaped) and preserving the
original keys.

In `@i18n/locales/ja.json`:
- Line 239: The translation for additionalBoorusPage.typeDescription contains a
stray "C" character ("フッターC"); update the value for the key
additionalBoorusPage.typeDescription to remove the extraneous character and
match the source intent (e.g., "通常、Booruウェブサイトのフッターに記載されています" or
"通常、Booruサイトのフッターに記載されています") so it reads correctly in Japanese.

In `@middleware/auth.ts`:
- Around line 5-6: The middleware currently uses
useRuntimeConfig().public.testAuthBypass to bypass auth (symbol:
useRuntimeConfig and key testAuthBypass in middleware/auth.ts) which is exposed
to the browser; replace this with a server-only compile-time or private check:
either gate the bypass with import.meta.env.TEST === 'true' (so the check runs
at build time) or read a server-only runtime value (not .public) such as
useRuntimeConfig().testAuthBypass that lives only on the server, and update the
if condition to use that server-only check so client-exposed public config is
never used for authorization.

In `@package.json`:
- Around line 63-69: Remove the unused jest devDependency from package.json:
delete the "jest" entry (currently "^30.3.0") since the repository runs tests
with Vitest (see "vitest" dependency and the test script using "vitest run") and
there are no jest imports or usage; also remove or repurpose jest.config.js if
it only contains Nuxt test utils and is not used by Jest to avoid confusion.
Ensure package-lock or pnpm/yarn lock is updated by reinstalling dependencies
after removing the entry.

In `@pages/premium/backup.vue`:
- Line 64: Replace the inline query string in the localePath call with an
explicit route object: call localePath({ path: '/premium/dashboard', query: {
restoreSuccess: 'true' } }) and assign that result to window.location.href
(keeping the full-page reload). Update the line using
localePath('/premium/dashboard?restoreSuccess=true') to use the route-object
form so query is passed explicitly and locale resolution remains consistent with
other pages.

In `@server/api/_sitemap-urls.ts`:
- Around line 20-22: Remove the dead commented line "// _sitemap: 'pages'" from
server/api/_sitemap-urls.ts since _i18nTransform: true now replaces that
behavior; in the function/config where _i18nTransform is set (look for the
object literal containing _i18nTransform: true), delete the obsolete comment so
the file contains only the active configuration.

In `@server/plugins/fix-canonical-queries.ts`:
- Around line 25-26: Replace the ad-hoc URLSearchParams parsing of the query
(the lines creating URL and reading tags from url.searchParams) with the shared
qs parser: import qs and use qs.parse on the query portion of event.path (or
event.url) to extract tags and other params, then assign the parsed.tags to the
tags variable so this plugin uses the same query-string parsing logic as the
rest of the repo; update any variable names as needed to use the parsed object
instead of url/searchParams.

In `@server/plugins/lcp-preload-fetchpriority.ts`:
- Around line 16-17: The plugin currently assumes response.body is full HTML and
proceeds to slice/splice the <head>, which can produce negative indexes and
corrupt non-HTML responses; after obtaining const body = response.body (and
confirming it's a string) check that the required markers (e.g., the index of
"<head", the index of "</head>" and the intended insertion point inside the
head) are all found (each index >= 0) before performing any rewrite, and if any
marker is missing return/bail without mutating response.body so only full HTML
responses are modified.

In `@test/pages/posts.test.ts`:
- Around line 22-26: The test route handler uses URLSearchParams on
route.request().url() to extract pageID, tags, and baseEndpoint; update it to
use the qs library so the test mirrors the app router (import qs from 'qs' at
the top), take the querystring portion from route.request().url() (e.g., split
at '?' or use URL to get search), call qs.parse(...) to get an object, and then
read pageID, tags, and baseEndpoint from that parsed object in the page.route
handler to replace the URLSearchParams usage.

In `@test/pages/premium/backup.test.ts`:
- Around line 15-40: The test currently only checks that Date.toLocaleString ran
(window.__backupDateFormattingCalls) but doesn't verify a backup artifact;
update the test to assert the actual export/download produced by the Backup
button. Replace the single click call with code that waits for a download or the
backend export request (e.g., use Playwright's page.waitForEvent('download') in
combination with page.locator('button', { hasText: 'Backup' }).click(), or
intercept the network request that returns the backup), then assert the download
object (download.suggestedFilename or saved file size) or the response
payload/storage key to ensure a real backup was created, while keeping the
existing checks for __backupDateFormattingCalls and page textContent('h1').

In `@test/server-mocks/plugin.ts`:
- Around line 8-20: The handler registered with defineNitroPlugin
(nitroApp.h3App.use and the inner defineEventHandler using getRequestURL)
currently falls through and implicitly returns undefined for unmatched /booru/*
paths; make this explicit by either returning a clear 404 error (e.g., throw
createError(...) or return a created error response) or adding a single-line
comment stating that undefined intentionally produces a 404, so readers see the
intent instead of an invisible implicit return; update the handler after the
/tags branch to include that explicit 404 behavior or comment and keep
mockPostsPage0 unchanged.

In `@test/server/fix-canonical-queries.test.ts`:
- Around line 12-14: getCanonical currently only matches rel="canonical"
followed immediately by href="..." with double quotes; update getCanonical to
locate a link element where rel="canonical" and capture the href value
regardless of attribute order or quote style by using a regex (or two-step
parse) that is case-insensitive, allows attributes in any order, accepts single
or double quotes, and captures the href group; specifically target the function
getCanonical to replace the strict /rel="canonical"\s+href="([^"]+)"/ match with
a pattern or logic that finds rel=canonical and then extracts the href value
robustly.

In `@vitest.config.ts`:
- Around line 13-17: The test config is missing required entries: set
fileParallelism: false inside the test block of defineConfig to force serial
execution and add runtimeConfig.public.apiUrl set to '' (or ensure
process.env.NUXT_PUBLIC_API_URL = '') under test (e.g.,
$test.runtimeConfig.public.apiUrl) so $fetch(baseURL: '') routes to the local
Nitro test server; update the test object in the exported defineConfig
accordingly (refer to the test block, fileParallelism, and
runtimeConfig.public.apiUrl symbols).

---

Outside diff comments:
In `@components/layout/navigation/Navbar.vue`:
- Around line 53-67: The aria-label on the menu toggle is static and always uses
$t('common.openMenu') even when isMenuActive is true; update the button's
aria-label to reflect state (use a ternary or computed property) so it reads
$t('common.openMenu') when isMenuActive is false and $t('common.closeMenu') when
isMenuActive is true—modify the button element that calls toggleMenu() and
references isMenuActive to use the dynamic label (or add a computed like
menuAriaLabel that returns the appropriate translation).

In `@components/pages/posts/navigation/search/SearchSelect.vue`:
- Around line 43-62: SearchSelect uses modelValue.label (hardcoded English)
inside the $t interpolation which prevents localized accessibility text; update
the parent that builds filterConfig to provide translated labels (call $t when
constructing each filterConfig.label) or change SearchSelect to accept a
labelKey prop (e.g., labelKey) instead of raw label and use $t(labelKey) where
modelValue.label is currently used; ensure the prop API and usages in the parent
page are updated consistently (replace references to modelValue.label with
$t(labelKey) or pass translated strings from parent) so the accessibility string
$t('search.changeFilter', { label }) receives a localized label.

In `@components/pages/posts/post/PostTag.vue`:
- Around line 48-65: The i18n function destructured as t from useI18n() is being
shadowed by the filter callback parameter in toggleBlockedTag; update the filter
call on customBlockList.value.filter((t) => t !== tag.name) (inside the
toggleBlockedTag function) to use a non-conflicting parameter name like item or
name (e.g., filter((name) => name !== tag.name)) so the i18n t remains
unshadowed and the comparison logic still excludes the tag.name correctly.

In `@components/pages/posts/PostsPageFooter.vue`:
- Around line 105-130: The NuxtLink paths for RELATED_TAGS and relatedBoorus
lose the current locale because they build raw string URLs; update both NuxtLink
:to bindings (the ones using selectedBooru.domain and booru.domain) to be route
objects that include the current locale (e.g., :to="{ path:
`/posts/${selectedBooru.domain}`, query: { tags: tag }, locale: $i18n.locale }"
for the RELATED_TAGS loop and :to="{ path: `/posts/${booru.domain}`, locale:
$i18n.locale }" for the relatedBoorus loop) or alternately use route names with
params and locale — reference RELATED_TAGS, formattedRelatedTags,
selectedBooru.domain, relatedBoorus, formattedTagsString and replace the
string-template :to values with locale-aware route objects so clicks preserve
the active locale.

In `@pages/index.vue`:
- Around line 78-110: The current flow returns errors from the fetch .catch but
only handles FetchError specially, so non-FetchError exceptions fall through and
you attempt to read response.data; refactor the fetch call into an explicit
try/catch around the await so all exceptions are handled uniformly: in the catch
block await import('@sentry/nuxt') and call Sentry.captureException(error),
clear searchTagResults.value (to avoid stale UI), and then if error is an
instance of FetchError run the existing switch/ toast logic (handling
404/429/default) and return early; for any other Error show a generic toast (or
reuse the default branch) and return so code after the fetch (the assignment to
searchTagResults.value = response.data) only runs on a successful response.
Ensure you reference response, FetchError, searchTagResults.value, and the
Sentry capture flow when making the change.

In `@test/pages/premium/backup.test.ts`:
- Around line 1-7: The test currently relies on createPage and setup but doesn't
explicitly enable Playwright or import debugBrowserOptions; update the spec to
import debugBrowserOptions (alongside defaultSetupConfig) and call await setup
with an explicit browser: true and the debugBrowserOptions merged into the
config (e.g., await setup({ ...defaultSetupConfig, browser: true,
browserOptions: debugBrowserOptions })) so the Playwright harness is obvious and
uses headful slowMo for debugging; ensure references to createPage and url
remain unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9c413f82-817d-4767-ac1c-14e458efa731

📥 Commits

Reviewing files that changed from the base of the PR and between 51dfd07 and e81f3d7.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (84)
  • AGENTS.md
  • app.vue
  • assets/js/SeoHelper.ts
  • assets/js/sidebarLinks.ts
  • components/PostPageError.vue
  • components/layout/FeedbackButton.vue
  • components/layout/PwaUpdater.vue
  • components/layout/Slideover.vue
  • components/layout/modal/FeedbackPrompt.vue
  • components/layout/modal/NewsletterPrompt.vue
  • components/layout/modal/PremiumPrompt.vue
  • components/layout/modal/PwaPrompt.vue
  • components/layout/modal/ReviewPrompt.vue
  • components/layout/navigation/BottomSheetWrapper.vue
  • components/layout/navigation/Navbar.vue
  • components/layout/navigation/Sidebar.vue
  • components/layout/navigation/SidebarWrapper.vue
  • components/pages/home/FeaturedTags.vue
  • components/pages/home/News.vue
  • components/pages/home/Newsletter.vue
  • components/pages/home/PageHistory.vue
  • components/pages/home/SimpleSearch.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/pages/posts/PromotedContent.vue
  • components/pages/posts/navigation/DomainSelector.vue
  • components/pages/posts/navigation/DomainSelectorFallback.vue
  • components/pages/posts/navigation/search/SearchMenu.vue
  • components/pages/posts/navigation/search/SearchMenuWrapper.vue
  • components/pages/posts/navigation/search/SearchSelect.vue
  • components/pages/posts/navigation/search/TagCollections.vue
  • components/pages/posts/post/PostChatWithAi.vue
  • components/pages/posts/post/PostComponent.vue
  • components/pages/posts/post/PostDownload.vue
  • components/pages/posts/post/PostMedia.vue
  • components/pages/posts/post/PostSave.vue
  • components/pages/posts/post/PostSaveFallback.vue
  • components/pages/posts/post/PostSource.vue
  • components/pages/posts/post/PostTag.vue
  • components/pages/settings/SettingSelect.vue
  • components/shared/PromotionalBanner.vue
  • components/shared/ScrollTopButton.vue
  • components/shared/ShareButton.vue
  • components/shared/ShowMore.vue
  • composables/locale.ts
  • composables/usePageHistory.ts
  • composables/useTagCollections.ts
  • composables/useTagTitle.ts
  • config/i18n.ts
  • config/promotions.ts
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/ja.json
  • i18n/locales/ru.json
  • layouts/default.vue
  • middleware/auth-redirect-if-logged-in.ts
  • middleware/auth.ts
  • nuxt.config.js
  • package.json
  • pages/cookie-policy.vue
  • pages/dmca.vue
  • pages/index.vue
  • pages/legal.vue
  • pages/other-sites.vue
  • pages/posts/[domain].vue
  • pages/premium/additional-boorus.vue
  • pages/premium/backup.vue
  • pages/premium/dashboard.vue
  • pages/premium/forgot-password.vue
  • pages/premium/index.vue
  • pages/premium/sign-in.vue
  • pages/premium/tag-collections.vue
  • pages/privacy-policy.vue
  • pages/terms-of-service.vue
  • server/api/_sitemap-urls.ts
  • server/plugins/fix-canonical-queries.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • test/helper.ts
  • test/pages/posts.test.ts
  • test/pages/premium/backup.test.ts
  • test/pages/premium/migrate-old-data.mock-data.ts
  • test/pages/premium/migrate-old-data.test.ts
  • test/server-mocks/plugin.ts
  • test/server/fix-canonical-queries.test.ts
  • vitest.config.ts
💤 Files with no reviewable changes (3)
  • test/pages/premium/migrate-old-data.test.ts
  • test/pages/premium/migrate-old-data.mock-data.ts
  • assets/js/SeoHelper.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{vue,tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components using their name without path prefix (e.g., <DomainSelector> not <Input/DomainSelector>) due to pathPrefix: false configuration

Files:

  • components/pages/posts/post/PostSave.vue
  • components/layout/FeedbackButton.vue
  • components/pages/settings/SettingSelect.vue
  • components/pages/home/SimpleSearch.vue
  • components/pages/posts/navigation/search/SearchSelect.vue
  • middleware/auth-redirect-if-logged-in.ts
  • components/PostPageError.vue
  • components/layout/navigation/BottomSheetWrapper.vue
  • components/layout/modal/NewsletterPrompt.vue
  • components/layout/modal/ReviewPrompt.vue
  • components/shared/ShareButton.vue
  • server/api/_sitemap-urls.ts
  • components/layout/Slideover.vue
  • components/layout/modal/FeedbackPrompt.vue
  • components/layout/navigation/SidebarWrapper.vue
  • pages/premium/forgot-password.vue
  • pages/dmca.vue
  • components/layout/modal/PwaPrompt.vue
  • components/shared/ScrollTopButton.vue
  • vitest.config.ts
  • components/pages/posts/post/PostChatWithAi.vue
  • pages/other-sites.vue
  • components/shared/ShowMore.vue
  • components/pages/home/News.vue
  • components/pages/posts/post/PostDownload.vue
  • test/helper.ts
  • pages/posts/[domain].vue
  • config/i18n.ts
  • layouts/default.vue
  • server/plugins/fix-canonical-queries.ts
  • components/pages/home/Newsletter.vue
  • pages/premium/backup.vue
  • pages/legal.vue
  • composables/useTagTitle.ts
  • pages/premium/sign-in.vue
  • test/pages/premium/backup.test.ts
  • test/server/fix-canonical-queries.test.ts
  • components/layout/PwaUpdater.vue
  • components/pages/home/PageHistory.vue
  • pages/terms-of-service.vue
  • components/pages/posts/navigation/DomainSelectorFallback.vue
  • components/pages/posts/post/PostSource.vue
  • components/pages/home/FeaturedTags.vue
  • components/layout/navigation/Navbar.vue
  • components/layout/navigation/Sidebar.vue
  • pages/cookie-policy.vue
  • components/pages/posts/navigation/search/SearchMenu.vue
  • components/pages/posts/navigation/DomainSelector.vue
  • components/pages/posts/navigation/search/SearchMenuWrapper.vue
  • pages/premium/additional-boorus.vue
  • composables/usePageHistory.ts
  • components/pages/posts/navigation/search/TagCollections.vue
  • assets/js/sidebarLinks.ts
  • components/layout/modal/PremiumPrompt.vue
  • components/pages/posts/post/PostTag.vue
  • config/promotions.ts
  • components/pages/posts/post/PostComponent.vue
  • test/server-mocks/plugin.ts
  • components/pages/posts/post/PostMedia.vue
  • pages/premium/tag-collections.vue
  • pages/privacy-policy.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • composables/useTagCollections.ts
  • components/shared/PromotionalBanner.vue
  • composables/locale.ts
  • components/pages/posts/post/PostSaveFallback.vue
  • app.vue
  • components/pages/posts/PromotedContent.vue
  • pages/premium/dashboard.vue
  • middleware/auth.ts
  • pages/index.vue
  • pages/premium/index.vue
  • components/pages/posts/PostsPageFooter.vue
  • test/pages/posts.test.ts
**/*.{js,ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Format code with Prettier using 120-char print width, no semicolons, single quotes, no trailing commas, and single attribute per line in Vue templates

Files:

  • components/pages/posts/post/PostSave.vue
  • components/layout/FeedbackButton.vue
  • components/pages/settings/SettingSelect.vue
  • components/pages/home/SimpleSearch.vue
  • components/pages/posts/navigation/search/SearchSelect.vue
  • middleware/auth-redirect-if-logged-in.ts
  • components/PostPageError.vue
  • components/layout/navigation/BottomSheetWrapper.vue
  • components/layout/modal/NewsletterPrompt.vue
  • components/layout/modal/ReviewPrompt.vue
  • components/shared/ShareButton.vue
  • server/api/_sitemap-urls.ts
  • components/layout/Slideover.vue
  • components/layout/modal/FeedbackPrompt.vue
  • components/layout/navigation/SidebarWrapper.vue
  • pages/premium/forgot-password.vue
  • pages/dmca.vue
  • components/layout/modal/PwaPrompt.vue
  • components/shared/ScrollTopButton.vue
  • vitest.config.ts
  • components/pages/posts/post/PostChatWithAi.vue
  • pages/other-sites.vue
  • components/shared/ShowMore.vue
  • components/pages/home/News.vue
  • components/pages/posts/post/PostDownload.vue
  • test/helper.ts
  • pages/posts/[domain].vue
  • config/i18n.ts
  • layouts/default.vue
  • server/plugins/fix-canonical-queries.ts
  • components/pages/home/Newsletter.vue
  • pages/premium/backup.vue
  • pages/legal.vue
  • composables/useTagTitle.ts
  • pages/premium/sign-in.vue
  • test/pages/premium/backup.test.ts
  • test/server/fix-canonical-queries.test.ts
  • components/layout/PwaUpdater.vue
  • components/pages/home/PageHistory.vue
  • pages/terms-of-service.vue
  • components/pages/posts/navigation/DomainSelectorFallback.vue
  • components/pages/posts/post/PostSource.vue
  • components/pages/home/FeaturedTags.vue
  • components/layout/navigation/Navbar.vue
  • components/layout/navigation/Sidebar.vue
  • pages/cookie-policy.vue
  • components/pages/posts/navigation/search/SearchMenu.vue
  • components/pages/posts/navigation/DomainSelector.vue
  • components/pages/posts/navigation/search/SearchMenuWrapper.vue
  • pages/premium/additional-boorus.vue
  • composables/usePageHistory.ts
  • components/pages/posts/navigation/search/TagCollections.vue
  • assets/js/sidebarLinks.ts
  • components/layout/modal/PremiumPrompt.vue
  • components/pages/posts/post/PostTag.vue
  • config/promotions.ts
  • components/pages/posts/post/PostComponent.vue
  • test/server-mocks/plugin.ts
  • components/pages/posts/post/PostMedia.vue
  • pages/premium/tag-collections.vue
  • pages/privacy-policy.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • composables/useTagCollections.ts
  • components/shared/PromotionalBanner.vue
  • composables/locale.ts
  • components/pages/posts/post/PostSaveFallback.vue
  • app.vue
  • components/pages/posts/PromotedContent.vue
  • pages/premium/dashboard.vue
  • middleware/auth.ts
  • pages/index.vue
  • pages/premium/index.vue
  • components/pages/posts/PostsPageFooter.vue
  • nuxt.config.js
  • test/pages/posts.test.ts
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use the qs library for query string parsing instead of standard URLSearchParams

Files:

  • middleware/auth-redirect-if-logged-in.ts
  • server/api/_sitemap-urls.ts
  • vitest.config.ts
  • test/helper.ts
  • config/i18n.ts
  • server/plugins/fix-canonical-queries.ts
  • composables/useTagTitle.ts
  • test/pages/premium/backup.test.ts
  • test/server/fix-canonical-queries.test.ts
  • composables/usePageHistory.ts
  • assets/js/sidebarLinks.ts
  • config/promotions.ts
  • test/server-mocks/plugin.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • composables/useTagCollections.ts
  • composables/locale.ts
  • middleware/auth.ts
  • nuxt.config.js
  • test/pages/posts.test.ts
vitest.config.ts

📄 CodeRabbit inference engine (AGENTS.md)

In test mode, set $test.runtimeConfig.public.apiUrl to an empty string so $fetch(baseURL: '') routes to the local Nitro test server

Disable Sentry source map uploads in tests by clearing environment variables in vitest.config.ts

Tests run serially without file parallelism

Files:

  • vitest.config.ts
config/i18n.ts

📄 CodeRabbit inference engine (AGENTS.md)

Define locales in config/i18n.ts as the single source of truth for i18n configuration

Files:

  • config/i18n.ts
server/plugins/fix-canonical-queries.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use the fix-canonical-queries.ts workaround plugin to patch canonical link tags in SSR output due to canonicalQueries being a no-op in i18n v10

Files:

  • server/plugins/fix-canonical-queries.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use @nuxt/test-utils with Playwright for tests by calling await setup({ browser: true }) within describe blocks

Import debugBrowserOptions from test/helper.ts in test files to enable headful playback with slowMo for debugging

Files:

  • test/pages/premium/backup.test.ts
  • test/server/fix-canonical-queries.test.ts
  • test/pages/posts.test.ts
package.json

📄 CodeRabbit inference engine (AGENTS.md)

Require Node ≥ 24 as specified in package.json engines field

Files:

  • package.json
test/server-mocks/plugin.ts

📄 CodeRabbit inference engine (AGENTS.md)

Mock server-side API calls in tests via a test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.js

Files:

  • test/server-mocks/plugin.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T08:20:24.292Z
Learning: Do not add service worker logic as the service worker is intentionally disabled (selfDestroying: true)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T08:20:24.292Z
Learning: Clone the repository with `--recursive` flag to include the Git submodule at `assets/lib/rule-34-shared-resources`
🪛 LanguageTool
AGENTS.md

[grammar] ~86-~86: Ensure spelling is correct
Context: ...st/helper.ts` for headful playback with slowMo. ### Docker production build - Multi-stage: b...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.22.1)
AGENTS.md

[warning] 53-53: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 56-56: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 61-61: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 66-66: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 69-69: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 72-72: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 75-75: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 80-80: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 88-88: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🔇 Additional comments (49)
components/pages/settings/SettingSelect.vue (1)

45-45: Good i18n-safe styling change by removing forced capitalization.

Dropping capitalize from both the trigger and option row avoids unintended casing transforms in localized strings and keeps translation output faithful.

Also applies to: 77-77

components/pages/posts/post/PostDownload.vue (1)

44-51: LGTM! Straightforward i18n substitution with no logic changes.

components/layout/navigation/BottomSheetWrapper.vue (1)

47-54: LGTM! Clean i18n substitution for the dismiss label.

components/pages/posts/navigation/DomainSelectorFallback.vue (1)

23-31: LGTM! Both accessibility strings correctly use i18n.

layouts/default.vue (1)

51-60: LGTM! Wrapping the console.info calls in import.meta.client correctly prevents SSR-side execution.

composables/useTagCollections.ts (1)

6-21: LGTM! Moving defaultTagCollections inside the composable to consume useI18n() is the correct pattern.

One minor note: the collection name strings are resolved once at composable initialization (snapshot of the active locale). If the app supports in-session locale switching, resetTagCollections() will always restore names in the locale that was active at mount, not the currently active one. This is typically acceptable, but worth being aware of if reactive locale switching is ever added.

middleware/auth-redirect-if-logged-in.ts (1)

1-16: LGTM! useLocalePath() is correctly called unconditionally at the top of the middleware before any early returns.

server/api/_sitemap-urls.ts (1)

7-13: Graceful degradation pattern is correct.

Initializing to an empty array and wrapping the Matomo fetch in a try/catch is the right approach — sitemap generation continues with zero dynamic entries rather than throwing and potentially crashing the entire sitemap handler.

test/helper.ts (1)

20-23: LGTM — switching to defaultBrowserOptions (headless) as the default is correct.

debugBrowserOptions remains exported and reachable for individual test debugging via page.pause().

i18n/locales/ru.json (1)

1-541: Russian locale looks structurally complete and consistent with other locales.

The 3-form plural for tagsInCollection ("тег | тега | тегов") is correctly defined for Russian pluralization rules in vue-i18n. The \\! escape pattern in promotional strings matches the same pattern used across all locale files and is correct for vue-i18n v9+ special-character escaping.

i18n/locales/es.json (1)

1-541: Spanish locale is structurally complete and consistent with other locale files.

Plural forms, interpolation placeholders, and @: linked message references all look correct. The \\! escape pattern in promotional strings matches the expected vue-i18n v9+ convention.

config/i18n.ts (1)

5-18: Nice centralization of locale config.

Deriving prefixedLocaleCodes and localeCodes from the same locales array keeps routing and runtime checks aligned. As per coding guidelines, "Define locales in config/i18n.ts as the single source of truth for i18n configuration".

composables/locale.ts (1)

4-15: Path normalization looks solid.

Sharing locale detection with config/i18n.ts and preserving the original query/hash should keep downstream route checks predictable.

components/pages/posts/post/PostSaveFallback.vue (1)

7-7: Good accessibility i18n update.

Localizing the aria-label keeps the icon-only button understandable for screen-reader users in every locale.

components/pages/posts/navigation/search/SearchMenuWrapper.vue (1)

54-54: Looks good.

Localizing the close control's accessible name is the right change for this dialog wrapper.

components/layout/modal/NewsletterPrompt.vue (1)

19-19: Nice i18n cleanup.

Switching the CTA text to $t(...) keeps this modal consistent with the rest of the localization pass.

components/shared/ScrollTopButton.vue (1)

44-44: LGTM!

Clean i18n substitution; $t is available globally from Nuxt i18n without an explicit useI18n() call in the script block.

components/layout/FeedbackButton.vue (1)

5-6: LGTM!

components/shared/ShowMore.vue (1)

59-59: LGTM!

components/layout/Slideover.vue (1)

61-61: LGTM!

components/PostPageError.vue (1)

17-18: LGTM!

components/pages/home/SimpleSearch.vue (1)

17-18: LGTM!

components/layout/modal/PremiumPrompt.vue (1)

11-11: LGTM — localePath correctly wraps internal routes for locale-aware navigation.

pages/terms-of-service.vue (1)

6-9: LGTM — computed() wrappers in useSeoMeta correctly make SEO metadata reactive to locale changes.

Note: the full page body (lines 18–158) remains hardcoded English while the title and meta description are now localized. A user browsing in a non-English locale will see a translated header but English content. This is a common trade-off for legal documents (professional legal translations are usually out of scope), so no action needed unless full translation coverage is planned.

pages/posts/[domain].vue (1)

247-248: LGTM — consistent with the $fetch + baseURL pattern used for other booru API calls.

pages/cookie-policy.vue (1)

4-15: LGTM — correct reactive i18n pattern for useSeoMeta.

components/shared/ShareButton.vue (1)

12-63: LGTM — clean i18n integration for toast messages and aria-label.

components/pages/posts/navigation/search/SearchMenu.vue (1)

35-393: LGTM — consistent t() usage throughout the template.

components/layout/PwaUpdater.vue (1)

17-30: LGTM — correct template-only $t() usage without needing useI18n() in script.

components/layout/modal/PwaPrompt.vue (1)

28-67: LGTM — correct $t() usage; good to see the iframe title attribute also localized.

pages/dmca.vue (1)

1-44: LGTM – i18n wiring for SEO meta and header is correct; computed(() => t(...)) pattern ensures reactivity on locale change.

components/layout/modal/FeedbackPrompt.vue (1)

25-69: LGTM – using $t() in template without pulling useI18n() into the script block is the correct approach here.

components/layout/navigation/SidebarWrapper.vue (1)

1-110: LGTM – using link.nameKey (the stable translation key string) as the v-for :key is the right choice for locale switching; localePath correctly leaves absolute external URLs unmodified.

pages/other-sites.vue (1)

1-61: LGTM – i18n integration for SEO meta and card content follows the established PR pattern.

components/pages/home/News.vue (1)

4-47: LGTM – wrapping newsList in computed() is the right approach to keep translations reactive on locale change.

pages/privacy-policy.vue (1)

4-10: LGTMuseI18n(), useLocalePath(), and all three localePath('/cookie-policy') link replacements are correct.

components/layout/modal/ReviewPrompt.vue (1)

25-26: No issue detected — code is correct.

The translation strings across all locales (en, es, ja, ru) use the placeholder {name}, and the code correctly passes { name: project.shortName }. The parameter name matches the translation placeholder; no substitution failure will occur.

			> Likely an incorrect or invalid review comment.
components/pages/home/PageHistory.vue (1)

1-116: LGTM!

localePath(path) correctly handles stored history paths (including embedded query strings) since @nuxtjs/i18n's localePath uses router.resolve() internally which parses query params from path strings. The navigateTo(localePath(path)) pattern and $t('common.removeHistoryItem') aria-label are both correct.

pages/premium/sign-in.vue (1)

1-151: LGTM!

The i18n integration is applied correctly: localePath is used for navigation and link hrefs, computed(() => t(...)) ensures reactive SEO meta updates on locale change, and navigateTo uses a proper route object with query separated from path.

components/pages/posts/navigation/DomainSelector.vue (1)

1-196: LGTM!

Domain-switcher toast, favicon alt text, premium badge, and "Add more Boorus" link copy are all correctly localized. The localePath ternary on line 186 is clean.

pages/premium/tag-collections.vue (1)

1-324: LGTM!

The $t(key, count) pluralization pattern on line 182 is correct for vue-i18n v9 (the number serves both as plural selector and {count} interpolation). The <i18n-t> usage for the exclude-tags hint with an inline <code> slot is the right approach for markup-embedded translations.

components/pages/posts/post/PostComponent.vue (1)

1-246: LGTM!

The tagTypeLabels computed correctly makes tag-category headings reactive to locale changes. The createTag helper cleanly centralizes Tag construction. The postSourceFileUrl computed correctly falls back to the video URL when no poster exists, and using the poster for PostSource on video posts is a sensible UX choice.

components/pages/posts/post/PostMedia.vue (1)

1-628: LGTM!

Template-level t() calls (error UI, play button, loading indicator, premium promo) are all reactive. The localePath calls with embedded query/hash strings in createVideoPlayer are handled correctly by Vue Router's string resolution. The non-reactive translations baked into the Fluid Player options at creation time are an inherent limitation of the imperative player API — not practically fixable without destroying/recreating the player on locale change.

components/pages/posts/post/PostSource.vue (1)

25-76: LGTM — the serviceName/action refactor and getServiceTitle helper are clean.

The TypeScript literal types ('find' as const, 'edit' as const) correctly narrow the union, and delegating label generation to a single translation call avoids duplicating display logic across two service lists.

pages/premium/additional-boorus.vue (1)

175-177: LGTM — computed(() => t(...)) is the correct pattern for reactive SSR-safe SEO titles.

components/pages/posts/post/PostChatWithAi.vue (1)

84-183: LGTM — i18n migration is consistent and the accessibility labels are correct.

pages/legal.vue (1)

1-93: LGTM — all links passed to localePath() are internal paths, and the computed(() => t(...)) SEO meta pattern is correct.

components/pages/posts/navigation/search/TagCollections.vue (1)

36-61: LGTM — toast/prompt strings are correctly localized and the TagCollection creation flow is unchanged.

assets/js/sidebarLinks.ts (1)

12-62: All consumers of sidebarNavigation are properly using .nameKey.

Both Sidebar.vue and SidebarWrapper.vue correctly access nameKey for keys and translations. The .name references found in premium/dashboard.vue and premium/index.vue are on separate local data structures, not on sidebarNavigation.

Comment thread AGENTS.md
Comment thread components/layout/navigation/Sidebar.vue
Comment thread components/pages/home/FeaturedTags.vue Outdated
Comment thread components/pages/posts/post/PostSave.vue Outdated
Comment thread components/pages/posts/PostsPageFooter.vue
Comment thread test/pages/posts.test.ts
Comment on lines +22 to +26
await page.route('**/booru/**/posts*', async (route) => {
const requestUrl = new URL(route.request().url())
const pageID = requestUrl.searchParams.get('pageID')
const tags = requestUrl.searchParams.get('tags')
const baseEndpoint = requestUrl.searchParams.get('baseEndpoint')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

# Check if qs is used in production code
rg "qs\." --type ts --type tsx --type js -B2 -A2 | head -50

Repository: Rule-34/App

Length of output: 83


🏁 Script executed:

# Find where qs is imported in the codebase
rg "import.*qs" --type ts --type tsx --type js

Repository: Rule-34/App

Length of output: 83


🏁 Script executed:

# Check the test file to see full context
cat test/pages/posts.test.ts

Repository: Rule-34/App

Length of output: 12783


🏁 Script executed:

# Search for URLSearchParams usage across test files
rg "URLSearchParams" --type ts --type tsx --type js

Repository: Rule-34/App

Length of output: 83


🏁 Script executed:

# Search for qs imports in production/source code
rg "from 'qs'|import.*qs" --type ts --type js

Repository: Rule-34/App

Length of output: 97


🏁 Script executed:

# Search for URLSearchParams usage in the codebase
rg "URLSearchParams" --type ts --type js -B2 -A2

Repository: Rule-34/App

Length of output: 702


🏁 Script executed:

# Check if qs is listed in package.json
cat package.json | grep -A 10 -B 10 '"qs"'

Repository: Rule-34/App

Length of output: 663


🏁 Script executed:

# Check what router.options.ts does with qs
cat app/router.options.ts

Repository: Rule-34/App

Length of output: 1711


🏁 Script executed:

# Search for booru posts API handling in production code
rg "booru.*posts|posts.*api" --type ts --type js -B3 -A3

Repository: Rule-34/App

Length of output: 6858


🏁 Script executed:

# Check usePageHistory.ts to understand why it uses URLSearchParams
head -50 composables/usePageHistory.ts

Repository: Rule-34/App

Length of output: 1384


🏁 Script executed:

# Search for other test files to see consistency in query parsing
rg "searchParams|qs\.parse" test/ --type ts --type js -B2 -A2

Repository: Rule-34/App

Length of output: 1018


Use qs library for query parsing to align with coding guidelines.

This test file uses URLSearchParams for query param extraction. Per coding guidelines for **/*.{ts,tsx,js} files, query string parsing should use the qs library. The app's router is configured with qs.parse as its official query parser, so test mocks should mirror this approach.

Suggested change
+import qs from 'qs'
+
     await page.route('**/booru/**/posts*', async (route) => {
       const requestUrl = new URL(route.request().url())
-      const pageID = requestUrl.searchParams.get('pageID')
-      const tags = requestUrl.searchParams.get('tags')
-      const baseEndpoint = requestUrl.searchParams.get('baseEndpoint')
+      const query = qs.parse(requestUrl.search.slice(1))
+      const pageID = typeof query.pageID === 'string' ? query.pageID : null
+      const tags = typeof query.tags === 'string' ? query.tags : null
+      const baseEndpoint = typeof query.baseEndpoint === 'string' ? query.baseEndpoint : null
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await page.route('**/booru/**/posts*', async (route) => {
const requestUrl = new URL(route.request().url())
const pageID = requestUrl.searchParams.get('pageID')
const tags = requestUrl.searchParams.get('tags')
const baseEndpoint = requestUrl.searchParams.get('baseEndpoint')
import qs from 'qs'
await page.route('**/booru/**/posts*', async (route) => {
const requestUrl = new URL(route.request().url())
const query = qs.parse(requestUrl.search.slice(1))
const pageID = typeof query.pageID === 'string' ? query.pageID : null
const tags = typeof query.tags === 'string' ? query.tags : null
const baseEndpoint = typeof query.baseEndpoint === 'string' ? query.baseEndpoint : null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/posts.test.ts` around lines 22 - 26, The test route handler uses
URLSearchParams on route.request().url() to extract pageID, tags, and
baseEndpoint; update it to use the qs library so the test mirrors the app router
(import qs from 'qs' at the top), take the querystring portion from
route.request().url() (e.g., split at '?' or use URL to get search), call
qs.parse(...) to get an object, and then read pageID, tags, and baseEndpoint
from that parsed object in the page.route handler to replace the URLSearchParams
usage.

Comment on lines 15 to +40
it('creates a backup', async () => {
const page = await createPage('/premium/backup')
const page = await createPage(url('/premium/backup'))

const pageErrors: string[] = []
page.on('pageerror', (error) => {
pageErrors.push(error.message)
})

await page.evaluate(() => {
const originalToLocaleString = Date.prototype.toLocaleString
;(window as any).__backupDateFormattingCalls = 0

await Promise.all([
//
page.locator('button', { hasText: 'Backup' }).click(),
Date.prototype.toLocaleString = function (...args: Parameters<Date['toLocaleString']>) {
;(window as any).__backupDateFormattingCalls++
return originalToLocaleString.apply(this, args)
}
})

page.waitForEvent('download')
])
await page.locator('button', { hasText: 'Backup' }).click()

// Assert createBackup logic ran and page stayed stable
const backupDateFormattingCalls = await page.evaluate(() => (window as any).__backupDateFormattingCalls)

expect(backupDateFormattingCalls).toBeGreaterThan(0)
expect(pageErrors).toEqual([])
expect(await page.textContent('h1')).toBe('Backup & Restore')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

This no longer proves that a backup was created.

__backupDateFormattingCalls > 0 only shows that some formatting ran after the click. The test will still pass if the export/download side effect regresses, so please keep an assertion on the actual backup artifact.

Suggested fix
-    await page.locator('button', { hasText: 'Backup' }).click()
+    const [download] = await Promise.all([
+      page.waitForEvent('download'),
+      page.locator('button', { hasText: 'Backup' }).click()
+    ])

     // Assert createBackup logic ran and page stayed stable
     const backupDateFormattingCalls = await page.evaluate(() => (window as any).__backupDateFormattingCalls)

+    expect(await download.suggestedFilename()).toMatch(/backup/i)
     expect(backupDateFormattingCalls).toBeGreaterThan(0)
     expect(pageErrors).toEqual([])
     expect(await page.textContent('h1')).toBe('Backup & Restore')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('creates a backup', async () => {
const page = await createPage('/premium/backup')
const page = await createPage(url('/premium/backup'))
const pageErrors: string[] = []
page.on('pageerror', (error) => {
pageErrors.push(error.message)
})
await page.evaluate(() => {
const originalToLocaleString = Date.prototype.toLocaleString
;(window as any).__backupDateFormattingCalls = 0
await Promise.all([
//
page.locator('button', { hasText: 'Backup' }).click(),
Date.prototype.toLocaleString = function (...args: Parameters<Date['toLocaleString']>) {
;(window as any).__backupDateFormattingCalls++
return originalToLocaleString.apply(this, args)
}
})
page.waitForEvent('download')
])
await page.locator('button', { hasText: 'Backup' }).click()
// Assert createBackup logic ran and page stayed stable
const backupDateFormattingCalls = await page.evaluate(() => (window as any).__backupDateFormattingCalls)
expect(backupDateFormattingCalls).toBeGreaterThan(0)
expect(pageErrors).toEqual([])
expect(await page.textContent('h1')).toBe('Backup & Restore')
it('creates a backup', async () => {
const page = await createPage(url('/premium/backup'))
const pageErrors: string[] = []
page.on('pageerror', (error) => {
pageErrors.push(error.message)
})
await page.evaluate(() => {
const originalToLocaleString = Date.prototype.toLocaleString
;(window as any).__backupDateFormattingCalls = 0
Date.prototype.toLocaleString = function (...args: Parameters<Date['toLocaleString']>) {
;(window as any).__backupDateFormattingCalls++
return originalToLocaleString.apply(this, args)
}
})
const [download] = await Promise.all([
page.waitForEvent('download'),
page.locator('button', { hasText: 'Backup' }).click()
])
// Assert createBackup logic ran and page stayed stable
const backupDateFormattingCalls = await page.evaluate(() => (window as any).__backupDateFormattingCalls)
expect(await download.suggestedFilename()).toMatch(/backup/i)
expect(backupDateFormattingCalls).toBeGreaterThan(0)
expect(pageErrors).toEqual([])
expect(await page.textContent('h1')).toBe('Backup & Restore')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/premium/backup.test.ts` around lines 15 - 40, The test currently
only checks that Date.toLocaleString ran (window.__backupDateFormattingCalls)
but doesn't verify a backup artifact; update the test to assert the actual
export/download produced by the Backup button. Replace the single click call
with code that waits for a download or the backend export request (e.g., use
Playwright's page.waitForEvent('download') in combination with
page.locator('button', { hasText: 'Backup' }).click(), or intercept the network
request that returns the backup), then assert the download object
(download.suggestedFilename or saved file size) or the response payload/storage
key to ensure a real backup was created, while keeping the existing checks for
__backupDateFormattingCalls and page textContent('h1').

Comment thread test/server-mocks/plugin.ts
Comment thread test/server/fix-canonical-queries.test.ts
Comment thread vitest.config.ts Outdated
Comment on lines +13 to +17
export default defineConfig({
test: {
testTimeout: 15000
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing required test-config entries per coding guidelines.

Two required settings are absent from the test block:

  1. Serial execution — without fileParallelism: false, test files can run concurrently, which can cause port conflicts or race conditions against the local Nitro dev server.
  2. Local API routing — the coding guideline requires setting $test.runtimeConfig.public.apiUrl to '' (equivalent: process.env.NUXT_PUBLIC_API_URL = '') so $fetch(baseURL: '') routes to the Nitro test server instead of the production URL.
🔧 Proposed fix
+process.env.NUXT_PUBLIC_API_URL = ''
+
 export default defineConfig({
   test: {
+    fileParallelism: false,
     testTimeout: 15000
   }
 })

As per coding guidelines: "Tests run serially without file parallelism" and "In test mode, set $test.runtimeConfig.public.apiUrl to an empty string so $fetch(baseURL: '') routes to the local Nitro test server."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` around lines 13 - 17, The test config is missing required
entries: set fileParallelism: false inside the test block of defineConfig to
force serial execution and add runtimeConfig.public.apiUrl set to '' (or ensure
process.env.NUXT_PUBLIC_API_URL = '') under test (e.g.,
$test.runtimeConfig.public.apiUrl) so $fetch(baseURL: '') routes to the local
Nitro test server; update the test object in the exported defineConfig
accordingly (refer to the test block, fileParallelism, and
runtimeConfig.public.apiUrl symbols).

AlejandroAkbal and others added 4 commits May 4, 2026 15:47
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Replace hardcoded UI text with i18n lookups and refactor SEO/title generation. Adds useI18n to posts and saved-posts pages, localizes filter labels, prompts, toasts, breadcrumbs and load messages. Introduces useTagTitle and stripLocaleFromPath/localePath usage to build locale-aware routes and SEO strings. Makes filterConfig reactive (computed), adjusts API fetch baseURL usage, and adds data-testid attributes for posts list and pagination. Adds locale selector and flag labels to settings, maps blocklist option labels to localized strings, and improves premium access check messaging.
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

Regarding the qs suggestion

CodeRabbit suggested using qs for query string parsing in several places. We considered it but decided against it for the following reasons:

  • Native API is sufficient. URLSearchParams (available in both browser and Nitro server environments) handles all the query parsing needs in this codebase correctly — including multi-value params and encoding.
  • Minimizing dependencies. The app already uses qs in one place (an older integration), but that doesn't justify proliferating it to new code. We prefer standard APIs where they do the job.
  • Nuxt compatibility. Native APIs integrate with Nuxt's SSR/client runtime better and avoid potential ESM/CJS interop headaches.

The URLSearchParams approach in usePageHistory, fix-canonical-queries, and test/pages/posts.test.ts remains as-is.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pages/settings.vue (1)

902-954: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor inconsistency: mixing t() and $t() in template.

The script uses t() (destructured from useI18n()), while the template mixes both t('...') and $t('...') (e.g., line 902 uses $t, line 954 uses $t, while most script-bound expressions use t). Both work, but consistency improves readability.

This is a style nit—no functional impact.

Also applies to: 1087-1087

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/settings.vue` around lines 902 - 954, Template mixes two i18n call
styles: the destructured t() from useI18n() and the global $t(), causing
inconsistent style; pick one (prefer using the destructured t) and replace
occurrences of $t('...') in the template with t('...') so all template
expressions consistently call t(), and ensure useI18n() is imported/destructured
where t is used.
pages/premium/backup.vue (1)

51-64: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle malformed backup JSON inside the restore error path.

A bad .json file throws at JSON.parse(...) before the try/catch, so the user gets neither errors.backupVersionNotSupported nor toasts.failedToRestoreBackup. Move parsing into the guarded block so invalid backups fail gracefully.

Suggested fix
-    const backupState: IBackupState = JSON.parse(await file.text())
-
     try {
+      const backupState: IBackupState = JSON.parse(await file.text())
       await tryToRestoreV2OrV3Backup(backupState)
     } catch (error) {
       const message =
         error instanceof Error && error.message === 'Backup version not supported'
           ? t('errors.backupVersionNotSupported')
           : String(error)
       toast.error(t('toasts.failedToRestoreBackup', { error: message }))
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/premium/backup.vue` around lines 51 - 64, The JSON.parse call for
backupState is currently outside the try/catch so malformed JSON throws before
error handling; move the parsing into the guarded block (or wrap it in the same
try) so that JSON.parse(await file.text()) is executed inside the try that calls
tryToRestoreV2OrV3Backup(backupState), allowing the catch to produce the
toast.error (and map the 'Backup version not supported' message) and return;
update references to backupState and ensure window.location.href only runs after
the guarded block succeeds.
♻️ Duplicate comments (1)
test/pages/premium/backup.test.ts (1)

15-49: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore a real backup artifact assertion.

__backupDateFormattingCalls > 0 only proves the filename-formatting branch ran. The test still passes if the actual export/download regresses, so it no longer verifies the user-visible outcome.

Suggested fix
-    await page.locator('button', { hasText: 'Backup' }).click()
-
-    // Wait for downloadBlob to execute (it's synchronous after click, but
-    // give the browser a tick to process the click event chain)
-    await page.waitForTimeout(1000)
+    const [download] = await Promise.all([
+      page.waitForEvent('download'),
+      page.locator('button', { hasText: 'Backup' }).click()
+    ])

     const backupDateFormattingCalls = await page.evaluate(() => (window as any).__backupDateFormattingCalls)

+    expect(await download.suggestedFilename()).toMatch(/backup/i)
     expect(backupDateFormattingCalls).toBeGreaterThan(0)
     expect(pageErrors).toEqual([])
     expect(await page.textContent('h1')).toBe('Backup & Restore')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/premium/backup.test.ts` around lines 15 - 49, The test only
verifies Date.toLocaleString was called, not that a real file download/export
occurred; replace the weak assertion with a real download check by waiting for
Playwright's download event when clicking the Backup button (use
page.waitForEvent('download') together with the click), then assert the Download
object was produced and its suggestedFilename (or saved path) is non-empty and
matches the expected pattern/extension; keep or remove the Date.prototype spy
but ensure you reference the createBackupState/downloadBlob flow so the test now
verifies an actual downloaded artifact was created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/pages/posts/PostsPageFooter.vue`:
- Around line 56-64: ratingLabel and sortLabel are interpolating raw query
values (props.selectedFilters.rating and props.selectedFilters.sort) into
translated strings, which exposes internal keys like "explicit" or "score";
update the computed properties (ratingLabel and sortLabel) to map those raw
values through the existing filters translations (e.g., use the filters.* keys
or a lookup like filters.rating[props.selectedFilters.rating] and
filters.sort[props.selectedFilters.sort]) and then pass the translated value
into t('seoFooter.sortingBy', ...) or use it directly so the footer shows
localized labels instead of raw enums; keep formattedCount as-is.
- Around line 39-48: hasTags is computed from the raw props.selectedTags which
stays true when all tags are exclusions (e.g. "-foo"), causing formattedTags to
be empty and leaving blank placeholders; change the logic to derive a filtered
tag list first (e.g. compute visibleTags by filtering out tags whose name
startsWith('-') and mapping with normalizeStringForTitle) and then make hasTags
depend on visibleTags.length, and use visibleTags in formattedTags (apply the
same change to the other computed block around the 74-88 area as well).

In `@composables/useTagTitle.ts`:
- Around line 38-45: The title building can start with the connector when
positive is empty; update useTagTitle.ts so the t('seoHelper.without') connector
is only prefixed with a separating space when title already contains positive
tags: check the current title/positive before appending the connector (i.e., add
the separator only if title is non-empty), then append negative.join(', ') as
before so no leading space or awkward phrasing occurs when all tags are
exclusions.

In `@i18n/locales/es.json`:
- Line 101: Remove the unnecessary backslash escape before the exclamation mark
in the Spanish localization strings so they don't render a literal backslash;
specifically update the "backupRestored" value to use "¡Copia de seguridad
restaurada exitosamente!" (remove the `\`), and likewise remove `\` before `!`
in the other Spanish keys flagged (the entries at the other occurrences noted
around lines 483 and 488) so all exclamation marks are plain characters in the
JSON values.

In `@i18n/locales/ru.json`:
- Line 146: The ru.json localization has mojibake in the "dmcaText" (and another
entry around the other reported location) — replace the corrupted Cyrillic
sequences: change "за��рос" to "запрос" and "л��цензии" to "лицензии" in the
corresponding JSON values (e.g., the "dmcaText" key and the other affected key
at the second location) and verify the file is saved with UTF-8 encoding to
prevent recurrence.
- Line 101: The Russian locale contains escaped exclamation marks (e.g.,
"backupRestored": "Резервная копия успешно восстановлена\\!") which render a
literal backslash in the UI; update the affected string values (including the
key "backupRestored" and the other occurrences around the reported ranges) by
removing the backslash and using a plain exclamation mark (replace '\!' with
'!') so the JSON values read "...!" and remain valid JSON strings.

In `@nuxt.config.js`:
- Around line 33-38: The mirroredRouteRules helper currently mirrors only
pageRouteRules and thus omits the root '/' rule for prefixed locales; update
mirroredRouteRules to accept and include the '/' path (fold '/' into the rules
set) and add a special-case when building entries so that when path === '/' you
emit `/${locale}` (no trailing slash) instead of `/${locale}/`; modify the logic
inside mirroredRouteRules (refer to the prefixedLocaleCodes and
Object.entries(rules) mapping) to handle this root-path branch while preserving
existing mappings for other paths.
- Around line 62-68: The head link array currently always includes a preconnect
entry using process.env.NUXT_PUBLIC_API_URL even when that env var is undefined;
update the code that builds the link array (the link variable in nuxt.config.js)
to only add or include the { rel: 'preconnect', href:
process.env.NUXT_PUBLIC_API_URL } entry when process.env.NUXT_PUBLIC_API_URL is
truthy (e.g., push it conditionally or filter out falsy entries) so no
meaningless head item is emitted in local/test setups.

In `@pages/index.vue`:
- Around line 92-99: The rate-limit toast action uses window.open in the onClick
handler (inside the case 429 branch where toast.error is called), which leaves
window.opener attached and allows reverse tabnabbing; update the onClick
implementation to open the external URL with noopener,noreferrer protection
(i.e., pass 'noopener,noreferrer' as the window.open features or otherwise null
out window.opener after opening) so the opened tab cannot access or manipulate
the original page.

In `@pages/premium/saved-posts/`[domain].vue:
- Around line 561-610: The shortTitle and titleForBody computed properties
duplicate logic from pages/posts/[domain].vue (building tag/title strings,
collecting filterParts) — extract this shared logic into a composable (e.g.,
usePostsTitle) that exposes functions or computed values like buildShortTitle
and buildTitleForBody so both pages can reuse it; move the common behavior that
uses buildTagTitle, selectedTags, selectedPage, selectedBooru, and
selectedFilters into that composable, keep only page-specific bits (like the
saved-posts extra selectedFilters.value.type handling) in the page component and
call the composable helpers to assemble the final title strings.

In `@server/plugins/lcp-preload-fetchpriority.ts`:
- Around line 33-35: The current code logs a warning whenever patchedHead ===
headHtml, which is the normal no-op path; remove the noisy console.warn and
either drop the log entirely or demote it to debug (e.g., console.debug) so
routine no-op responses don't flood logs; update the block in the
lcp-preload-fetchpriority module where patchedHead and headHtml are compared
(the if that currently calls console.warn) to either remove the warning or
condition it on a new check (e.g., only log if preload links were present but
still unchanged).

In `@test/server/fix-canonical-queries.test.ts`:
- Around line 19-46: The test suite hardcodes the production origin in several
expect(getCanonical(html)).toBe(...) assertions; update those to use
project.urls.production.origin so tests don't break if the origin changes:
replace the literal 'https://r34.app' in the assertions inside the tests that
call getCanonical (the tests labeled "includes tags in canonical when present",
"encodes pipe characters in tags", "strips non-canonical params (page) while
keeping tags", "strips all params when tags is absent", and "does not append
tags to non-posts pages") with `${project.urls.production.origin}` (or
string-concatenate the origin with the path/query) so the expected canonical is
built from project.urls.production.origin plus the path/query rather than a
hardcoded URL.

---

Outside diff comments:
In `@pages/premium/backup.vue`:
- Around line 51-64: The JSON.parse call for backupState is currently outside
the try/catch so malformed JSON throws before error handling; move the parsing
into the guarded block (or wrap it in the same try) so that JSON.parse(await
file.text()) is executed inside the try that calls
tryToRestoreV2OrV3Backup(backupState), allowing the catch to produce the
toast.error (and map the 'Backup version not supported' message) and return;
update references to backupState and ensure window.location.href only runs after
the guarded block succeeds.

In `@pages/settings.vue`:
- Around line 902-954: Template mixes two i18n call styles: the destructured t()
from useI18n() and the global $t(), causing inconsistent style; pick one (prefer
using the destructured t) and replace occurrences of $t('...') in the template
with t('...') so all template expressions consistently call t(), and ensure
useI18n() is imported/destructured where t is used.

---

Duplicate comments:
In `@test/pages/premium/backup.test.ts`:
- Around line 15-49: The test only verifies Date.toLocaleString was called, not
that a real file download/export occurred; replace the weak assertion with a
real download check by waiting for Playwright's download event when clicking the
Backup button (use page.waitForEvent('download') together with the click), then
assert the Download object was produced and its suggestedFilename (or saved
path) is non-empty and matches the expected pattern/extension; keep or remove
the Date.prototype spy but ensure you reference the
createBackupState/downloadBlob flow so the test now verifies an actual
downloaded artifact was created.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01a2525c-b06b-4092-91f3-1171c08c4254

📥 Commits

Reviewing files that changed from the base of the PR and between e81f3d7 and a4b11a8.

📒 Files selected for processing (27)
  • AGENTS.md
  • components/layout/navigation/Navbar.vue
  • components/layout/navigation/Sidebar.vue
  • components/pages/home/FeaturedTags.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/pages/posts/PromotedContent.vue
  • components/pages/posts/post/PostSave.vue
  • components/pages/posts/post/PostTag.vue
  • components/shared/PromotionalBanner.vue
  • composables/useTagTitle.ts
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/ja.json
  • i18n/locales/ru.json
  • jest.config.js
  • nuxt.config.js
  • package.json
  • pages/index.vue
  • pages/posts/[domain].vue
  • pages/premium/backup.vue
  • pages/premium/saved-posts/[domain].vue
  • pages/settings.vue
  • server/api/_sitemap-urls.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • test/pages/premium/backup.test.ts
  • test/server-mocks/plugin.ts
  • test/server/fix-canonical-queries.test.ts
💤 Files with no reviewable changes (1)
  • jest.config.js
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components without path prefix using their component name (e.g., <DomainSelector> instead of <Input/DomainSelector>) due to pathPrefix: false configuration in Nuxt

Files:

  • server/api/_sitemap-urls.ts
  • components/pages/posts/post/PostSave.vue
  • test/server-mocks/plugin.ts
  • components/pages/posts/PromotedContent.vue
  • test/server/fix-canonical-queries.test.ts
  • components/pages/home/FeaturedTags.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • components/shared/PromotionalBanner.vue
  • composables/useTagTitle.ts
  • pages/settings.vue
  • components/pages/posts/post/PostTag.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/layout/navigation/Navbar.vue
  • pages/index.vue
  • test/pages/premium/backup.test.ts
  • pages/premium/saved-posts/[domain].vue
  • components/layout/navigation/Sidebar.vue
  • pages/premium/backup.vue
  • pages/posts/[domain].vue
**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use camelCase for variable and function names throughout JavaScript/TypeScript code

Omit semicolons from the end of statements, as per Prettier configuration

Files:

  • server/api/_sitemap-urls.ts
  • test/server-mocks/plugin.ts
  • test/server/fix-canonical-queries.test.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • composables/useTagTitle.ts
  • test/pages/premium/backup.test.ts
  • nuxt.config.js
**/*.{ts,tsx,js,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use single quotes in code instead of double quotes, as configured in Prettier

Remove trailing commas from code, as per Prettier configuration

Files:

  • server/api/_sitemap-urls.ts
  • components/pages/posts/post/PostSave.vue
  • test/server-mocks/plugin.ts
  • components/pages/posts/PromotedContent.vue
  • test/server/fix-canonical-queries.test.ts
  • components/pages/home/FeaturedTags.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • components/shared/PromotionalBanner.vue
  • composables/useTagTitle.ts
  • pages/settings.vue
  • components/pages/posts/post/PostTag.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/layout/navigation/Navbar.vue
  • pages/index.vue
  • test/pages/premium/backup.test.ts
  • pages/premium/saved-posts/[domain].vue
  • components/layout/navigation/Sidebar.vue
  • pages/premium/backup.vue
  • nuxt.config.js
  • pages/posts/[domain].vue
**/*.{ts,tsx,js,vue,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Limit print width to 120 characters per line when formatting with Prettier

Files:

  • server/api/_sitemap-urls.ts
  • components/pages/posts/post/PostSave.vue
  • test/server-mocks/plugin.ts
  • components/pages/posts/PromotedContent.vue
  • test/server/fix-canonical-queries.test.ts
  • components/pages/home/FeaturedTags.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • components/shared/PromotionalBanner.vue
  • composables/useTagTitle.ts
  • pages/settings.vue
  • components/pages/posts/post/PostTag.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/layout/navigation/Navbar.vue
  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/ru.json
  • package.json
  • pages/index.vue
  • test/pages/premium/backup.test.ts
  • pages/premium/saved-posts/[domain].vue
  • i18n/locales/ja.json
  • components/layout/navigation/Sidebar.vue
  • pages/premium/backup.vue
  • nuxt.config.js
  • pages/posts/[domain].vue
**/server/api/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Define Nitro server API routes in server/api/ directory following standard API route conventions

Files:

  • server/api/_sitemap-urls.ts
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use single attribute per line in Vue template markup

Files:

  • components/pages/posts/post/PostSave.vue
  • components/pages/posts/PromotedContent.vue
  • components/pages/home/FeaturedTags.vue
  • components/shared/PromotionalBanner.vue
  • pages/settings.vue
  • components/pages/posts/post/PostTag.vue
  • components/pages/posts/PostsPageFooter.vue
  • components/layout/navigation/Navbar.vue
  • pages/index.vue
  • pages/premium/saved-posts/[domain].vue
  • components/layout/navigation/Sidebar.vue
  • pages/premium/backup.vue
  • pages/posts/[domain].vue
**/test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Mock server-side API calls via test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.js

Files:

  • test/server-mocks/plugin.ts
  • test/server/fix-canonical-queries.test.ts
  • test/pages/premium/backup.test.ts
**/test/**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use @nuxt/test-utils with Playwright browser mode inside describe blocks that call await setup({ browser: true })

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo during test debugging

Files:

  • test/server/fix-canonical-queries.test.ts
  • test/pages/premium/backup.test.ts
**/server/plugins/**/*.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Register Nitro plugins in server/plugins/ directory for server-side initialization

Files:

  • server/plugins/lcp-preload-fetchpriority.ts
**/composables/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Register custom Vue composables in composables/ directory as they are auto-imported by Nuxt

Files:

  • composables/useTagTitle.ts
**/locales/**/*.json

📄 CodeRabbit inference engine (AGENTS.md)

Maintain i18n locale JSON files in locales/ directory for supported languages (en, ru, es, ja)

Files:

  • i18n/locales/en.json
  • i18n/locales/es.json
  • i18n/locales/ru.json
  • i18n/locales/ja.json
**/nuxt.config.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Non-default locale routes (ru, es, ja) must have URL prefixes configured via mirroredRouteRules() helper to ensure prefixed paths get the same caching/SSR rules as non-prefixed routes

Use custom imgproxy provider for <NuxtImg> components and generate images at 1x density only in webp format to reduce bandwidth

In test mode, set $test.runtimeConfig.public.apiUrl to empty string so $fetch(baseURL: '') routes to local Nitro test server

Fully disable Sentry in tests via $test.sentry.enabled: false in nuxt.config.js

Files:

  • nuxt.config.js
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Enable Sentry source map uploads only in production Docker builds using `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` build arguments
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Use multi-stage Docker build where build stage needs `SENTRY_*` build args for source map uploads and production stage copies only `.output/` directory
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Set `NITRO_PRESET` build argument to select the deployment target in Docker production builds
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Require Node version ≥ 24 as specified in `package.json` engines field
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Clone repository with `--recursive` flag to initialize the Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T09:57:10.860Z
Learning: Configure the app to call a separate API service at `NUXT_PUBLIC_API_URL` environment variable (default `http://localhost:8081`)
🪛 LanguageTool
AGENTS.md

[grammar] ~82-~82: Ensure spelling is correct
Context: .... Do not add service worker logic. ### TailwindCSS Tailwind v4 uses CSS-based config (`asse...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~102-~102: Ensure spelling is correct
Context: ...st/helper.ts` for headful playback with slowMo. ### Docker production build - Multi-stage: ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (18)
package.json (1)

61-68: LGTM — jest removed, @nuxtjs/i18n and @nuxt/test-utils correctly added.

The devDependency cleanup (removing unused jest in favour of vitest) and the new @nuxtjs/i18n production dependency align with the PR's i18n rollout.

i18n/locales/en.json (1)

1-529: LGTM — all previously flagged typos and \! escape issues resolved.

The "Can't", "don't", and all \\! escape sequences have been corrected. Linked messages (@:...), plural forms, and interpolation placeholders are consistent throughout.

components/pages/posts/post/PostSave.vue (1)

69-83: LGTM — conditional aria-label correctly reflects save/unsave state.

Both common.savePost and common.unsavePost keys are defined in all locale files. The button now announces the correct action to screen-reader users.

components/pages/home/FeaturedTags.vue (1)

85-109: LGTM — localePath correctly guarded behind isExternalHref, undefined used for non-external attrs.

Both issues from the previous review (locale-prefixing external URLs and dangling rel/target attributes) are resolved. The alt text is properly localized.

components/pages/posts/post/PostTag.vue (1)

48-79: LGTM — i18n wiring is correct in both script and template.

t() from useI18n() for toast calls and $t() in the template are both valid patterns. All translation keys are present in the locale files.

components/pages/posts/PromotedContent.vue (1)

77-95: LGTM — localePath now correctly guarded against external promo URLs.

The conditional isExternalHref check on href, target, and rel addresses the past review concern. Localized alt text and premium CTA copy are wired correctly.

components/layout/navigation/Navbar.vue (1)

13-26: LGTM — locale-aware home detection and route-base-name approach for posts page are both correct.

localePath('/') properly handles locale-prefixed home paths, and getRouteBaseName(route) strips locale suffixes from route names — consistent with the same pattern in composables/usePageHistory.ts. The v-intersection-observer simplification (handler directly, no array wrapper) is valid per @vueuse/components API.

i18n/locales/ja.json (1)

1-542: LGTM!

The Japanese locale file is comprehensive and well-structured. The previously flagged translation artifact (フッターCフッター) on line 240 has been corrected. The seoHelper connector strings (lines 538-540) properly support the locale-aware title building in useTagTitle.

composables/useTagTitle.ts (1)

26-28: The empty cleanedTags guard was correctly added.

This addresses the previously flagged issue where buildTitle could return '' instead of null when all tag names normalized to null.

pages/settings.vue (1)

55-64: Label-to-value reverse mapping is correct but fragile.

The onSelectedListChange function receives a display label and reverse-maps it to the enum value. This works but creates a coupling between UI labels and logic. If a translation changes to match another option's label, the mapping could fail silently.

The current implementation is acceptable given the small option set and explicit mapping. No immediate action needed.

pages/posts/[domain].vue (3)

138-187: Good: filterConfig is now computed for locale reactivity.

Making filterConfig a computed property ensures filter labels update when the locale changes, which is the correct pattern for i18n-dependent configuration objects.


251-264: API fetch pattern uses relative URL with baseURL correctly.

The refactored fetch uses a relative endpoint (/booru/${...}/tags) with baseURL: config.public.apiUrl, which aligns with the PR's API architecture changes and works correctly in both SSR and client contexts.


868-876: The pattern of using useI18n() inside definePageMeta.validate is valid and works correctly. Nuxt 3 supports composables in validation contexts during SSR. The same pattern is already used elsewhere in the codebase (e.g., pages/premium/saved-posts/[domain].vue), confirming this approach is established and functional.

pages/premium/saved-posts/[domain].vue (1)

116-178: LGTM: filterConfig is correctly computed for locale reactivity.

AGENTS.md (1)

1-114: LGTM: Comprehensive project documentation.

The AGENTS.md file provides excellent onboarding documentation covering the stack, setup, architecture, and conventions. The previously flagged markdown formatting (blank lines after headings) has been addressed. The static analysis warnings about "TailwindCSS" and "slowMo" are false positives—these are correct technical terms.

server/api/_sitemap-urls.ts (2)

7-13: Good: Graceful degradation when Matomo API fails.

The try/catch wrapper ensures sitemap generation continues even if the Matomo keyword fetch fails. The warning log provides visibility into the failure without breaking the sitemap endpoint.


15-22: LGTM: _i18nTransform: true enables locale-aware sitemap URLs.

The switch from _sitemap: 'pages' to _i18nTransform: true correctly integrates with @nuxtjs/sitemap + @nuxtjs/i18n to generate localized alternate URLs for each dynamic tag page.

test/server-mocks/plugin.ts (1)

1-23: LGTM: Well-documented test mock plugin.

The plugin correctly mocks /booru/**/posts and /booru/**/tags endpoints for SSR tests. The comment on lines 20-21 explicitly documents the 404 behavior for unmatched paths, addressing the previous review feedback.

Comment thread components/pages/posts/PostsPageFooter.vue Outdated
Comment thread components/pages/posts/PostsPageFooter.vue Outdated
Comment thread composables/useTagTitle.ts
Comment thread i18n/locales/es.json Outdated
Comment thread i18n/locales/ru.json Outdated
Comment thread nuxt.config.js
Comment thread pages/index.vue
Comment on lines +561 to 610
const shortTitle = computed(() => {
const hasTags = selectedTags.value.length > 0
const hasPaging = selectedPage.value !== selectedBooru.value.type.initialPageID

return title
})
let title = hasPaging ? t('posts.seo.pageOf', { page: selectedPage.value }) : ''

const shortTitle = computed(() => {
let _title = completeTitle.value
if (hasTags) {
const tagTitle = buildTagTitle(selectedTags.value)
title += t('posts.seo.taggedHentai', { tags: tagTitle })
} else {
title += t('posts.seo.savedPosts')

_title = _title.replace(/Posts tagged/, '')
_title = _title.replace(/with /, '')
_title = _title.replace(/and ?without /, ' w/o ')
_title = _title.replace(/with a score of/, 'score')
const filterParts: string[] = []
if (selectedFilters.value.type) filterParts.push(t('posts.seo.typeOnly', { type: selectedFilters.value.type }))
if (selectedFilters.value.rating) filterParts.push(t('posts.seo.rated', { rating: selectedFilters.value.rating }))
if (selectedFilters.value.sort) filterParts.push(t('posts.seo.sortedBy', { sort: selectedFilters.value.sort }))
if (selectedFilters.value.score) filterParts.push(t('posts.seo.scoreOf', { score: selectedFilters.value.score }))
if (filterParts.length) title += ', ' + filterParts.join(', ')

if (selectedTags.value.length > 0) {
_title = _title.replace(/, from .+$/, '')
title += t('posts.seo.fromDomain', { domain: selectedBooru.value.domain })
}

_title = _title.trim()
// Capitalize first letter - https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
_title = _title.charAt(0).toUpperCase() + _title.slice(1)

return _title
return title.trim()
})

const titleForBody = computed(() => {
let _title = completeTitle.value

// TODO: Show page number in body title
_title = _title.replace(/page \d+ of /i, '')

_title = _title.replace(/saved posts/i, '')

_title = _title.replace(/tagged with/i, '')

_title = _title.replace(/hentai videos, GIFs, and images/i, 'rule 34 hentai')

_title = _title.replace(/, from .+$/, '')

// Edge case: ", sorted by" || ", rated" || ", with a score of"
if (_title.startsWith(', ')) {
_title = _title.slice(2)
}

_title = _title.trim()
// Capitalize first letter - https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
_title = _title.charAt(0).toUpperCase() + _title.slice(1)

return _title
const hasTags = selectedTags.value.length > 0

if (hasTags) {
const tagTitle = buildTagTitle(selectedTags.value)
const title = t('posts.seo.tagsRule34', { tags: tagTitle })
const filterParts: string[] = []
if (selectedFilters.value.type) filterParts.push(t('posts.seo.typeOnly', { type: selectedFilters.value.type }))
if (selectedFilters.value.rating) filterParts.push(t('posts.seo.rated', { rating: selectedFilters.value.rating }))
if (selectedFilters.value.sort) filterParts.push(t('posts.seo.sortedBy', { sort: selectedFilters.value.sort }))
if (selectedFilters.value.score) filterParts.push(t('posts.seo.scoreOf', { score: selectedFilters.value.score }))
const full = filterParts.length ? `${title}, ${filterParts.join(', ')}` : title
return full.charAt(0).toUpperCase() + full.slice(1)
}

const filterParts: string[] = []
if (selectedFilters.value.type) filterParts.push(t('posts.seo.typeOnly', { type: selectedFilters.value.type }))
if (selectedFilters.value.rating) filterParts.push(t('posts.seo.rated', { rating: selectedFilters.value.rating }))
if (selectedFilters.value.sort) filterParts.push(t('posts.seo.sortedBy', { sort: selectedFilters.value.sort }))
if (selectedFilters.value.score) filterParts.push(t('posts.seo.scoreOf', { score: selectedFilters.value.score }))
if (!filterParts.length) return ''

const joined = filterParts.join(', ')
return joined.charAt(0).toUpperCase() + joined.slice(1)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

Significant code duplication with pages/posts/[domain].vue.

The shortTitle and titleForBody computed properties duplicate ~50 lines of logic from the main posts page. The saved-posts version adds selectedFilters.value.type handling but otherwise follows the same pattern.

Consider extracting a shared composable (e.g., usePostsTitle) if these pages continue to evolve together. For now, this is acceptable given the slight differences in filter handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pages/premium/saved-posts/`[domain].vue around lines 561 - 610, The
shortTitle and titleForBody computed properties duplicate logic from
pages/posts/[domain].vue (building tag/title strings, collecting filterParts) —
extract this shared logic into a composable (e.g., usePostsTitle) that exposes
functions or computed values like buildShortTitle and buildTitleForBody so both
pages can reuse it; move the common behavior that uses buildTagTitle,
selectedTags, selectedPage, selectedBooru, and selectedFilters into that
composable, keep only page-specific bits (like the saved-posts extra
selectedFilters.value.type handling) in the page component and call the
composable helpers to assemble the final title strings.

Comment thread server/plugins/lcp-preload-fetchpriority.ts
Comment thread test/server/fix-canonical-queries.test.ts Outdated
…en security, backup JSON.parse, hasTags, locale escapes, nitpicks
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
composables/useTagTitle.ts (1)

42-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the extra separator before seoHelper.without.

seoHelper.without already includes surrounding spaces in the locale dictionaries, so if (title) title += ' ' turns mixed titles into double-spaced output and still leaves a leading space in all-negative cases. Let the translation own the spacing, or trim the final result before returning it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composables/useTagTitle.ts` around lines 42 - 45, In useTagTitle (where
variables title and negative are assembled and t('seoHelper.without') is
appended), remove the manual insertion of a space ("if (title) title += ' '")
because the locale string already includes spacing; also ensure the function
returns title.trim() (trim the final result) to guard against any
leading/trailing spaces for all-negative or mixed cases.
components/pages/posts/PostsPageFooter.vue (2)

43-50: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Derive hasTags from the filtered tag list, not from the fallback string.

Comparing formattedTags[0] to t('seoFooter.defaultTag') misclassifies real searches like tags=anime as “no tags”, because the explicit tag normalizes to the same text as the fallback. Build one filtered-and-normalized array (dropping nulls from normalizeStringForTitle) and use its length for both hasTags and the fallback decision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/PostsPageFooter.vue` around lines 43 - 50, The current
computed formattedTags and hasTags logic compares formattedTags.value[0] to
t('seoFooter.defaultTag'), which misclassifies real tags that normalize to the
same text as the fallback; instead, produce a single filtered-and-normalized
array from props.selectedTags by filtering out tags with names starting with '-'
and mapping through normalizeStringForTitle while removing any null/empty
results, then use that array's length to set hasTags (hasTags.value =
filtered.length > 0) and return filtered.length > 0 ? filtered :
[t('seoFooter.defaultTag')] from formattedTags so both computed properties
derive from the same source.

66-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Translate the sort value before inserting it into localized copy.

props.selectedFilters.sort is still the raw query enum here, so the footer can render internal values like score inside translated paragraphs. Map it through the existing filters.sortBy* labels first, the same way ratingLabel already localizes rating values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/pages/posts/PostsPageFooter.vue` around lines 66 - 69, The
sortLabel computed currently injects the raw props.selectedFilters.sort enum
into the translator, causing internal values like "score" to appear; update the
computed (sortLabel) to first map props.selectedFilters.sort to the localized
label from the existing filters.sortBy* mapping (the same approach used by
ratingLabel) and then pass that mapped string into t('seoFooter.sortingBy', {
sort: mappedLabel }) so the footer displays the translated sort name instead of
the raw enum.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pages/index.vue`:
- Around line 93-99: The toast error currently opens the status page by
concatenating config.public.apiUrl + '/status', which can produce
"undefined/status"; change it to compute a safe base = config.public.apiUrl ||
window.location.origin and build the status URL with a proper resolver (e.g.,
new URL('/status', base).toString()) before passing it to window.open in the
toast.action.onClick so the link always resolves to the correct origin; update
the code around toast.error (the toast.action handler) to use this resolved URL.

In `@test/server/fix-canonical-queries.test.ts`:
- Around line 54-55: The test's hreflang extraction (variables alternateTags and
codes) is brittle because it assumes a specific attribute order and double-quote
style; update the regex to the same lookahead-style matcher used in
getCanonical() so it finds hreflang regardless of attribute order and whether
single or double quotes are used: first match alternate link tags robustly (like
the existing alternateTags logic) then extract the hreflang value using a
lookahead/optional-quote pattern equivalent to getCanonical() so the test
accepts semantically identical SSR output serialized differently.

---

Duplicate comments:
In `@components/pages/posts/PostsPageFooter.vue`:
- Around line 43-50: The current computed formattedTags and hasTags logic
compares formattedTags.value[0] to t('seoFooter.defaultTag'), which
misclassifies real tags that normalize to the same text as the fallback;
instead, produce a single filtered-and-normalized array from props.selectedTags
by filtering out tags with names starting with '-' and mapping through
normalizeStringForTitle while removing any null/empty results, then use that
array's length to set hasTags (hasTags.value = filtered.length > 0) and return
filtered.length > 0 ? filtered : [t('seoFooter.defaultTag')] from formattedTags
so both computed properties derive from the same source.
- Around line 66-69: The sortLabel computed currently injects the raw
props.selectedFilters.sort enum into the translator, causing internal values
like "score" to appear; update the computed (sortLabel) to first map
props.selectedFilters.sort to the localized label from the existing
filters.sortBy* mapping (the same approach used by ratingLabel) and then pass
that mapped string into t('seoFooter.sortingBy', { sort: mappedLabel }) so the
footer displays the translated sort name instead of the raw enum.

In `@composables/useTagTitle.ts`:
- Around line 42-45: In useTagTitle (where variables title and negative are
assembled and t('seoHelper.without') is appended), remove the manual insertion
of a space ("if (title) title += ' '") because the locale string already
includes spacing; also ensure the function returns title.trim() (trim the final
result) to guard against any leading/trailing spaces for all-negative or mixed
cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05d1a8c6-a8d3-4488-a274-7335aaea464a

📥 Commits

Reviewing files that changed from the base of the PR and between a4b11a8 and 6c02b06.

📒 Files selected for processing (9)
  • components/pages/posts/PostsPageFooter.vue
  • composables/useTagTitle.ts
  • i18n/locales/es.json
  • i18n/locales/ru.json
  • nuxt.config.js
  • pages/index.vue
  • pages/premium/backup.vue
  • server/plugins/lcp-preload-fetchpriority.ts
  • test/server/fix-canonical-queries.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components without path prefix (e.g., use <DomainSelector> not <Input/DomainSelector>) because components are auto-imported flat via pathPrefix: false in nuxt.config.js

For images via <NuxtImg>, use the custom imgproxy provider; deliberately generate images at 1x density only in webp format to reduce bandwidth

Files:

  • composables/useTagTitle.ts
  • test/server/fix-canonical-queries.test.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • components/pages/posts/PostsPageFooter.vue
  • pages/index.vue
  • pages/premium/backup.vue
**/*.{js,ts,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • composables/useTagTitle.ts
  • test/server/fix-canonical-queries.test.ts
  • server/plugins/lcp-preload-fetchpriority.ts
  • components/pages/posts/PostsPageFooter.vue
  • pages/index.vue
  • pages/premium/backup.vue
  • i18n/locales/es.json
  • i18n/locales/ru.json
  • nuxt.config.js
composables/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use composables from the composables/ directory for shared Vue logic; they are auto-imported by Nuxt

Files:

  • composables/useTagTitle.ts
test/**/*.{ts,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Mock server-side API calls in tests via a test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.js; set $test.runtimeConfig.public.apiUrl to empty string so $fetch routes to the local Nitro test server

Files:

  • test/server/fix-canonical-queries.test.ts
test/**/*.{test,spec}.ts

📄 CodeRabbit inference engine (AGENTS.md)

In browser-based tests using @nuxt/test-utils and Playwright, call await setup({ browser: true }) inside describe blocks

For debug mode in browser tests, import debugBrowserOptions from test/helper.ts for headful playback with slowMo

Files:

  • test/server/fix-canonical-queries.test.ts
{nuxt.config.ts,server/plugins/**/*.ts,server/middleware/**/*.ts}

📄 CodeRabbit inference engine (AGENTS.md)

Do not add service worker logic; the service worker is intentionally disabled via selfDestroying: true

Files:

  • server/plugins/lcp-preload-fetchpriority.ts
components/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Organize components in the components/ directory to be auto-imported flat; do not rely on nested folder structures for imports

Files:

  • components/pages/posts/PostsPageFooter.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T12:40:25.694Z
Learning: Clone the repository with `--recursive` flag to include the Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-04T12:40:25.694Z
Learning: 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
🔇 Additional comments (2)
server/plugins/lcp-preload-fetchpriority.ts (2)

9-13: Locale-aware route gating looks correct.

Using stripLocaleFromPath(event.path) before the /posts/ check is a solid way to make the rewrite work consistently for localized routes.


16-25: Head rewrite safety checks are in good shape.

The string-body guard plus <head> marker validation and no-op early return make this rewrite path robust and avoid unintended response mutation.

Also applies to: 33-37

Comment thread pages/index.vue
Comment thread test/server/fix-canonical-queries.test.ts Outdated
…ion, hreflang regex, rate-limit URL, useTagTitle spacing
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
test/server/fix-canonical-queries.test.ts (1)

54-61: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Hreflang regex robustness — past comment now addressed; consider tightening the assertion.

The alternateTags and codes regexes now use lookaheads and handle both quote styles, so the past review comment about attribute-order brittleness appears resolved.

However, expect.arrayContaining(expected) only verifies that every expected code is present; it won't catch duplicate hreflang entries or unexpected extra codes introduced by a regression. A stricter variant catches both:

🔎 Stricter assertion option
-    expect(codes).toEqual(expect.arrayContaining(expected))
+    expect(codes.sort()).toEqual([...expected].sort())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/server/fix-canonical-queries.test.ts` around lines 54 - 61, The current
assertion using expect.arrayContaining(expected) only checks presence; update
the test to ensure no duplicates or unexpected hreflang values by adding checks
against the codes variable: (1) assert that codes contains all expected entries
(keep the existing expect.arrayContaining(expected)), (2) assert codes.length
equals expected.length to prevent extra entries, and (3) assert new
Set(codes).size equals expected.length to prevent duplicates; reference the
alternateTags, codes, and expected variables when making these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/pages/posts/PostsPageFooter.vue`:
- Around line 62-75: ratingLabel and sortLabel can build translation keys from
arbitrary query values; add validation and a safe fallback before calling t().
For ratingLabel, restrict props.selectedFilters.rating (raw) to the allowed set
(e.g., ['explicit','safe','questionable']) and if it’s not in the set use
'explicit' (or a configured default) when building key for t(); for sortLabel,
if props.selectedFilters.sort is missing or not in the allowed sort values
(e.g., ['mostRecent','mostViewed','mostLiked']), fallback to returning
t('seoFooter.mostPopularUploads') or build the key from a default sort value
instead of using the raw value. Update the computed properties ratingLabel and
sortLabel to validate raw against these allowed arrays and only construct t(key)
for known values, otherwise use the fallback translation.

In `@composables/useTagTitle.ts`:
- Around line 21-24: The two chained .map calls that build cleanedTags
(currently mapping tag.name then normalizing) should be collapsed into a single
pass: replace the .map((tag) => tag.name).map((tag) =>
normalizeStringForTitle(tag)) with one .map((tag) =>
normalizeStringForTitle(tag.name)) and keep the existing .filter((tag): tag is
string => tag != null) afterwards; update references to cleanedTags and ensure
normalizeStringForTitle is called with tag.name so types remain correct.
- Around line 4-12: The JSDoc for useTagTitle incorrectly claims it uses
seoHelper.with and seoHelper.and but the implementation only references
seoHelper.without; either remove "with" and "and" from the comment or implement
those connectors. Fix by editing the useTagTitle documentation to only mention
"without" (and locale usage via seoHelper.*), or if the intent was to support
"with" and "and", update the buildTagTitle logic inside useTagTitle to call
t('seoHelper.with') and t('seoHelper.and') where connectors are composed
alongside the existing t('seoHelper.without'); ensure the docstring and
implementation stay consistent.
- Around line 42-44: In useTagTitle.ts within the code that builds the title
(the block using the title variable and negative array), ensure you don't rely
on translated strings to carry surrounding spaces: change the concatenation to
insert an explicit separator (e.g., add a single space before or after calling
t('seoHelper.without') when appending to title) so the code reads clearly
(reference: the title variable and negative.join usage) — alternatively document
the whitespace contract in JSDoc for the function if you prefer keeping
translations responsible for spacing.

In `@pages/index.vue`:
- Around line 67-76: Replace the deprecated params option with query in the
$fetch call in pages/index.vue: locate the call that builds the params object
(containing baseEndpoint: selectedBooru.value.domain, tag, order: 'count',
limit: 20, and httpScheme: selectedBooru.value.config?.options?.HTTPScheme) and
rename that property to query so the same key/value shape is passed via query
instead of params; keep all field names and values identical and remove any
remaining usage of params for this request.

---

Duplicate comments:
In `@test/server/fix-canonical-queries.test.ts`:
- Around line 54-61: The current assertion using
expect.arrayContaining(expected) only checks presence; update the test to ensure
no duplicates or unexpected hreflang values by adding checks against the codes
variable: (1) assert that codes contains all expected entries (keep the existing
expect.arrayContaining(expected)), (2) assert codes.length equals
expected.length to prevent extra entries, and (3) assert new Set(codes).size
equals expected.length to prevent duplicates; reference the alternateTags,
codes, and expected variables when making these assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a43ee5f8-e148-484e-8be9-db1d0d55e908

📥 Commits

Reviewing files that changed from the base of the PR and between 6c02b06 and 986ab40.

📒 Files selected for processing (4)
  • components/pages/posts/PostsPageFooter.vue
  • composables/useTagTitle.ts
  • pages/index.vue
  • test/server/fix-canonical-queries.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx,vue,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for code formatting with settings: 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates

Files:

  • test/server/fix-canonical-queries.test.ts
  • composables/useTagTitle.ts
  • components/pages/posts/PostsPageFooter.vue
  • pages/index.vue
test/**/*.test.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use @nuxt/test-utils with Playwright inside describe blocks that call await setup({ browser: true })

For browser test debugging, import debugBrowserOptions from test/helper.ts for headful playback with slowMo

Files:

  • test/server/fix-canonical-queries.test.ts
composables/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Import shared Vue composables from composables/ directory (auto-imported by Nuxt)

Place shared Vue composables in composables/ directory for auto-import by Nuxt

Files:

  • composables/useTagTitle.ts
components/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Components must be imported without path prefix (register as <DomainSelector> not <Input/DomainSelector>)

Files:

  • components/pages/posts/PostsPageFooter.vue
components/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Images should be generated at 1x density only in webp format via custom imgproxy provider to reduce bandwidth

Files:

  • components/pages/posts/PostsPageFooter.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T07:53:03.940Z
Learning: Disable service worker functionality (do not add service worker logic) — the service worker is intentionally disabled with `selfDestroying: true`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T07:53:03.940Z
Learning: Clone the repository with `--recursive` flag to include Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T07:53:03.940Z
Learning: Configure external API calls to use `NUXT_PUBLIC_API_URL` environment variable (default `http://localhost:8081`)
🔇 Additional comments (2)
components/pages/posts/PostsPageFooter.vue (1)

43-54: Good fix on excluded-tag handling and fallback tags.

Deriving hasTags from includedTags and defaulting formattedTags avoids empty {tags} interpolation when only excluded tags are present.

pages/index.vue (1)

92-99: Resolve the status-page URL from an absolute origin.

new URL('/status', config.public.apiUrl) still assumes config.public.apiUrl is absolute. If this is configured as a path such as /booru, the URL() constructor can throw and the toast action stops working. Resolve the API base against window.location.origin before building /status. (developer.mozilla.org)

Suggested change
               action: {
                 label: t('toasts.verifyNotBot'),
-                onClick: () =>
-                  window.open(new URL('/status', config.public.apiUrl).toString(), '_blank', 'noopener,noreferrer')
+                onClick: () => {
+                  const apiBase = new URL(config.public.apiUrl || '/', window.location.origin)
+                  window.open(new URL('/status', apiBase).toString(), '_blank', 'noopener,noreferrer')
+                }
               }

Comment thread components/pages/posts/PostsPageFooter.vue
Comment thread composables/useTagTitle.ts
Comment thread composables/useTagTitle.ts
Comment thread composables/useTagTitle.ts
Comment thread pages/index.vue Outdated
…, explicit spacing, params->query, hreflang assertion
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Fixed 2 file(s) based on 3 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@i18n/locales/en.json`:
- Line 510: The localization string key allSourcedP2 contains an awkward
interpolation order; change the sentence so {domain} appears after the item list
and {tags} integrates naturally — e.g., reword to something like: "Discover
{count} hentai illustrations, porn gifs, and XXX animations from {domain},
including {tags}." Update the value for "allSourcedP2" accordingly to preserve
placeholders {count}, {domain}, and {tags}.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb1e6cd9-f6a8-4fae-9fab-897ff3038c37

📥 Commits

Reviewing files that changed from the base of the PR and between 172f5e9 and 97dc42a.

📒 Files selected for processing (2)
  • i18n/locales/en.json
  • i18n/locales/es.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,jsx,ts,tsx,vue,json,css,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Use 120-character print width for code formatting (enforced by Prettier)

Files:

  • i18n/locales/en.json
  • i18n/locales/es.json
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T10:49:34.063Z
Learning: Images should be generated at 1x density only in webp format via the custom imgproxy provider to reduce bandwidth
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T10:49:34.063Z
Learning: Do not add service worker logic — the service worker is intentionally disabled with `selfDestroying: true`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T10:49:34.063Z
Learning: Import shared resources from the Git submodule at `assets/lib/rule-34-shared-resources` (clone repository with `--recursive` flag)
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T10:49:34.063Z
Learning: In production Docker builds, source map uploads to Sentry only occur when `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` build args are provided
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T10:49:34.063Z
Learning: Use custom scroll behavior that skips scroll-to-top when only the `page` query param changes between same-route navigations
🔇 Additional comments (2)
i18n/locales/es.json (1)

1-540: Localization file looks consistent and previously flagged issues are fixed.

I re-checked token interpolation patterns, JSON validity, and prior regressions (\!, Reséñanos, {count} {domain}), and this revision looks good to merge.

i18n/locales/en.json (1)

221-221: ⚡ Quick win

The {code} placeholder is properly handled. The Vue component pages/premium/tag-collections.vue uses the correct i18n-t pattern with a named template slot that provides <code>-</code> as the replacement value, so this string will correctly render as "Use - to exclude tags" in the UI.

Comment thread i18n/locales/en.json Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • i18n/locales/en.json

Commit: ecc1b7976dbae064b107c548c7b0aaf8e6d48c2b

The changes have been pushed to the i18n branch.

Time taken: 2m 47s

coderabbitai Bot and others added 2 commits May 5, 2026 11:09
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Workaround for @nuxtjs/i18n v10 dropping query params from canonical URLs: update clarifying comments in nuxt.config.js and expand the server plugin docs, and add a client-side re-apply of the canonical link. pages/posts/[domain].vue now computes a canonical URL that includes the `tags` query and uses useHead to restore it after client hydration. server/plugins/fix-canonical-queries.ts comment was expanded to describe the two-part fix and includes a removal checklist and upstream tracking link.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nuxt.config.js`:
- Around line 85-87: The Open Graph meta entry for property 'og:image' currently
uses a relative path '/social.jpg' which should be an absolute URL; update the
meta config that defines { property: 'og:image', content: '/social.jpg' } to
provide a full https://... URL (preferably derived from your canonical site URL
or runtime BASE_URL variable) so previews use a stable absolute image URL
alongside the existing 'og:image:width' and 'og:image:height' entries.

In `@pages/posts/`[domain].vue:
- Around line 695-699: The SEO/body builders are interpolating raw query slugs
(e.g., selectedFilters.value.sort) into localized strings via t(), leaking
internal slugs to users; create and reuse a small helper (e.g.,
translateFilterValue or getLocalizedFilterLabel) that maps each filter key
(sort, rating, score) and its value to the correct localized label using the
same i18n keys you use elsewhere, then call that helper before passing values
into t() for shortTitle, titleForBody and description so all three builders
display translated filter values consistently.
- Around line 811-820: The breadcrumb currently hardcodes '/' for the home URL
in defineBreadcrumb's itemListElement, which breaks locale-prefixed sites;
update the first item's item value to be locale-aware by using the i18n helper
(e.g. localePath('/') or useLocalePath('/') from your i18n composables) or by
constructing the home path with the current locale (using route or
useI18n().locale.value) instead of '/'; modify the item in defineBreadcrumb
where selectedBooru and route are used so the home link points to the current
locale.
- Around line 883-891: The premium gating logic inside the page validation
should be removed so validate (definePageMeta().validate) does not return an
error object; instead, delete the block that reads useUserData()/if
(!isPremium.value && booru.isPremium) { ... return { status: 401 ... } } and
move this check into the existing middleware (the fallback-booru/upsell
middleware) so that it inspects useUserData(), booru.isPremium and uses
useI18n() there to redirect or show upsell flows; ensure validate simply returns
true (or is omitted) so middleware can run.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 24555256-3b67-4718-b5ff-93f3267e80c2

📥 Commits

Reviewing files that changed from the base of the PR and between 97dc42a and 48bbb3c.

📒 Files selected for processing (4)
  • i18n/locales/en.json
  • nuxt.config.js
  • pages/posts/[domain].vue
  • server/plugins/fix-canonical-queries.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Components should be imported without path prefix in Nuxt (e.g., <DomainSelector> not <Input/DomainSelector>)

Files:

  • server/plugins/fix-canonical-queries.ts
  • pages/posts/[domain].vue
**/*.{vue,ts,tsx,js,jsx,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • server/plugins/fix-canonical-queries.ts
  • i18n/locales/en.json
  • nuxt.config.js
  • pages/posts/[domain].vue
server/plugins/**

📄 CodeRabbit inference engine (AGENTS.md)

Server plugins must be located in server/plugins/ and follow Nitro plugin conventions

Files:

  • server/plugins/fix-canonical-queries.ts
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript for all project files in the src/Nuxt application

Files:

  • server/plugins/fix-canonical-queries.ts
  • pages/posts/[domain].vue
server/plugins/fix-canonical-queries.ts

📄 CodeRabbit inference engine (AGENTS.md)

Canonical queries workaround must be implemented via server/plugins/fix-canonical-queries.ts plugin to patch the canonical <link> in SSR output since canonicalQueries in i18n module config is a no-op in v10

Files:

  • server/plugins/fix-canonical-queries.ts
nuxt.config.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Non-default locales (ru, es, ja) must get URL prefixes, with route rules mirrored via the mirroredRouteRules() helper

In test mode, use $test.runtimeConfig.public.apiUrl set to '' so $fetch(baseURL: '') routes to the local Nitro test server

Disable Sentry in tests via $test.sentry.enabled: false in nuxt.config.js

The custom scroll behavior should skip scroll-to-top when only the page query param changes between same-route navigations

Files:

  • nuxt.config.js
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use custom imgproxy provider for <NuxtImg> components and deliberately generate images at 1x density only in webp format

Files:

  • pages/posts/[domain].vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T20:24:11.825Z
Learning: Do not add service worker logic; the service worker is intentionally disabled with `selfDestroying: true`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T20:24:11.825Z
Learning: Node version must be ≥ 24 as specified in `package.json` engines field
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T20:24:11.825Z
Learning: Git submodule at `assets/lib/rule-34-shared-resources` must be cloned with `--recursive` flag
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T20:24:11.825Z
Learning: External API calls must be made to the service at `NUXT_PUBLIC_API_URL` (default `http://localhost:8081`), with separate API codebase at github.com/Rule-34/API

Comment thread nuxt.config.js Outdated
Comment thread pages/posts/[domain].vue Outdated
Comment thread pages/posts/[domain].vue
Comment thread pages/posts/[domain].vue Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 4 unresolved review comments.

Files modified:

  • nuxt.config.js
  • pages/posts/[domain].vue

Commit: 4abb856e7a1050e1121a4cc7713e40e526feb419

The changes have been pushed to the i18n branch.

Time taken: 5m 57s

Fixed 2 file(s) based on 4 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nuxt.config.js (1)

158-165: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Declare testAuthBypass in the base runtimeConfig.public.

testAuthBypass is only introduced under $test.runtimeConfig.public (Line 428) and is not declared in the base runtimeConfig.public (Lines 161-164). Nuxt's runtime config best practice is to declare every key at the base level so it gets typed, picked up by NUXT_PUBLIC_* env overrides, and is consistent across environments. Add a default of false (or undefined) at the base so the key is always defined.

♻️ Proposed change
   runtimeConfig: {
     matomoApiKey: undefined,

     public: {
       apiUrl: undefined,
-      sentryDsn: undefined
+      sentryDsn: undefined,
+      testAuthBypass: false
     }
   },

Also applies to: 425-430

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nuxt.config.js` around lines 158 - 165, Add the testAuthBypass key to the
base runtimeConfig.public so it is always declared and typed (matching the
$test.runtimeConfig.public addition); update the runtimeConfig.public object
(the same block that contains apiUrl and sentryDsn) to include testAuthBypass
with a default value (false or undefined) so NUXT_PUBLIC_* env overrides and
type inference work correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pages/posts/`[domain].vue:
- Around line 711-775: Extract the repeated "filterParts" assembly into a helper
(e.g. buildSeoFilterParts) that takes selectedFilters (and
t/getLocalizedFilterLabel or access them via closure) and returns the array or
joined string of localized filter fragments; replace the repeated blocks inside
the computed properties shortTitle, titleForBody and description with calls to
buildSeoFilterParts (use a variant or a flag for description-specific phrasing
if needed) so each computed uses the single helper along with existing helpers
buildTagTitle and getLocalizedFilterLabel to keep behavior identical.

---

Outside diff comments:
In `@nuxt.config.js`:
- Around line 158-165: Add the testAuthBypass key to the base
runtimeConfig.public so it is always declared and typed (matching the
$test.runtimeConfig.public addition); update the runtimeConfig.public object
(the same block that contains apiUrl and sentryDsn) to include testAuthBypass
with a default value (false or undefined) so NUXT_PUBLIC_* env overrides and
type inference work correctly.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 376487e5-3221-4512-a43d-42b5b7a9d1a7

📥 Commits

Reviewing files that changed from the base of the PR and between 48bbb3c and 4abb856.

📒 Files selected for processing (2)
  • nuxt.config.js
  • pages/posts/[domain].vue
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,vue,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Nuxt 4 with Vue 3 and TypeScript for the SSR application with Nitro server

Files:

  • nuxt.config.js
  • pages/posts/[domain].vue
**/*.{ts,tsx,vue,js,jsx,css,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for code formatting with 120-char print width, no semicolons, single quotes, and trailing commas removed

Files:

  • nuxt.config.js
  • pages/posts/[domain].vue
**/*.{css,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use TailwindCSS v4 via @tailwindcss/vite Vite plugin with CSS-based config (NOT PostCSS)

Files:

  • pages/posts/[domain].vue
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use single attribute per line in Vue templates for Prettier formatting

Files:

  • pages/posts/[domain].vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T20:56:38.913Z
Learning: Clone the repository with --recursive flag to include the Git submodule at assets/lib/rule-34-shared-resources
🔇 Additional comments (8)
nuxt.config.js (5)

36-44: Root-path mirroring is correctly handled.

The path === '/' branch produces /${locale} (no trailing slash) for prefixed locale roots, and pageRouteRules now includes / so localized home pages inherit the same cacheHeaders. This correctly resolves the prior concern.


73-75: Conditional preconnect emission looks good.

The preconnect entry is now only appended when NUXT_PUBLIC_API_URL is set, avoiding meaningless head items in local/test setups.


85-85: og:image is now an absolute URL.

Using project.urls.production.origin (no trailing slash) yields a stable absolute URL for Open Graph previews, addressing the prior feedback.


190-218: 💤 Low value

No action required — the experimental.strictSeo object shape is valid in @nuxtjs/i18n v10.

The type signature correctly supports boolean | SeoAttributesOptions, and the configuration as written won't trigger type errors. The existing inline comments adequately document that canonicalQueries is ignored at runtime and reference the SSR/CSR workarounds in place.


141-150: ⚡ Quick win

This review concern is based on incorrect assumptions about Nuxt 4's config merging behavior.

Nuxt 4 concatenates (appends) array values in $test overrides rather than replacing them. Both ~/server/plugins/lcp-preload-fetchpriority and ~/test/server-mocks/plugin.ts will be registered during vitest runs. The premise that the base plugin is "silently dropped" is incorrect.

			> Likely an incorrect or invalid review comment.
pages/posts/[domain].vue (3)

762-775: ⚡ Quick win

No issue found — buildTagTitle returns null for empty tags, not an empty string.

The useTagTitle composable explicitly returns null when the tags array is empty or after normalization yields no tags (see the return null conditions and || null at the end). The nullish-coalescing operator on Line 765 therefore correctly falls back to t('posts.seo.descriptionVarious') when no tags are selected. The code is correct as written.

			> Likely an incorrect or invalid review comment.

767-769: ⚡ Quick win

The code is correct as written. The descriptionScoreOf key is intentionally authored with a leading separator embedded in the localized string (e.g., ", with a score of {score}" in English, "、スコア:{score}" in Japanese). This is a valid design pattern where the separator is moved into the i18n resource instead of hardcoded in the template. No fix is required.

			> Likely an incorrect or invalid review comment.

783-796: ⚡ Quick win

This is a documented temporary workaround; verify the two-part fix is properly scoped before ship.

The two concerns are worth checking:

  1. Canonical only carries tags. Confirmed intentional — page, filter.rating, filter.sort, filter.score are intentionally dropped per the canonicalQueries: ['tags'] config in nuxt.config.js. Verify this SEO posture (every page in a tag-search shares the same canonical) aligns with your indexing strategy.

  2. Duplicate canonical is not a risk in this implementation. The codebase does not use useLocaleHead; it calls useHead directly with a custom canonical. The server plugin (server/plugins/fix-canonical-queries.ts) and client override (pages/posts/[domain].vue) form a documented workaround for a known @nuxtjs/i18n v10 bug where canonicalQueries is non-functional. A removal checklist is in place for when upstream fixes the issue.

Confirm baseUrl is correctly configured in nuxt.config.js (it is: project.urls.production) and that this two-part fix is the intended long-term strategy until the upstream bug is resolved.

Comment thread pages/posts/[domain].vue
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • pages/posts/[domain].vue

Commit: 256035ad67c8032502f52ad4062536f4487f2325

The changes have been pushed to the i18n branch.

Time taken: 4m 13s

coderabbitai Bot and others added 3 commits May 5, 2026 21:13
Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Add server-only SEO/meta handling and update docs.

- app.vue: set up useSeoMeta inside an import.meta.server guard, use useRequestURL().origin to build an absolute og:image and add ogImageWidth/Height (ensures OG image is correct and absolute during SSR).
- nuxt.config.js: remove og:image meta entries from global head (they ran too early to know request host).
- AGENTS.md: document the i18n canonicalQueries workaround (SSR + CSR steps), add an SEO & Head Management section explaining global vs page tags, requirement for absolute OG images, and that canonicals should point to production.

These changes ensure correct Open Graph URLs and canonical behavior across SSR/CSR and provide guidance for removal when upstream fixes are available.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 48: The table entry currently shows `locales/` but the actual directory
is `i18n/locales/`; update the table cell that contains the string `locales/` to
`i18n/locales/` (ensure the exact table row with the description "i18n JSON
files (en, ru, es, ja)" now references `i18n/locales/` so contributors are
pointed to the correct location).
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b607cd8f-23b7-451c-85c3-648c280ce16e

📥 Commits

Reviewing files that changed from the base of the PR and between 4abb856 and 35aa2d3.

📒 Files selected for processing (4)
  • AGENTS.md
  • app.vue
  • nuxt.config.js
  • pages/posts/[domain].vue
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,jsx,tsx,vue,json,css}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier for code formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • nuxt.config.js
  • pages/posts/[domain].vue
  • app.vue
nuxt.config.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Apply i18n URL prefixes and route rules using the mirroredRouteRules() helper to ensure prefixed paths get the same caching/SSR rules as the default locale

Place static global SEO tags (favicon, rating, monetization, color-scheme) in nuxt.config.js under head.meta

In test mode, set $test.runtimeConfig.public.apiUrl to an empty string so $fetch(baseURL: '') routes to the local Nitro test server

Disable Sentry fully in tests by setting $test.sentry.enabled: false in nuxt.config.js

Implement custom scroll behavior that skips scroll-to-top when only the page query param changes between same-route navigations

Files:

  • nuxt.config.js
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Import Vue components without path prefix (e.g., <DomainSelector> not <Input/DomainSelector>) due to auto-import configuration with pathPrefix: false

Build OG image URLs dynamically with useRequestURL().origin on the server only and ensure they are absolute URLs, since Open Graph requires absolute URLs

Use project.urls.production for canonical URLs to point to production even when served from clone domains, as canonicals prevent duplicate content for SEO

Use the imgproxy custom provider for <NuxtImg> components and generate images at 1x density only in webp format

Files:

  • pages/posts/[domain].vue
  • app.vue
**/pages/**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

For page-specific tags (title, description), use useSeoMeta in the page component

Files:

  • pages/posts/[domain].vue
**/app.vue

📄 CodeRabbit inference engine (AGENTS.md)

Use useSeoMeta for dynamic global tags that need the request host, placed inside an if (import.meta.server) guard in app.vue, not in nuxt.config.js

Files:

  • app.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T21:28:46.553Z
Learning: Do not add service worker logic as the service worker is intentionally disabled with `selfDestroying: true`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T21:28:46.553Z
Learning: Enable Sentry source map uploads only in production Docker builds with `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` build arguments
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T21:28:46.553Z
Learning: Clone the repository with `--recursive` flag to include the Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T21:28:46.553Z
Learning: Use Node ≥ 24 as specified in `package.json` engines field
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-05T21:28:46.553Z
Learning: Use centralized project configuration from `config/project.ts` for branding/URLs and `config/i18n.ts` for locales
🪛 LanguageTool
AGENTS.md

[grammar] ~95-~95: Ensure spelling is correct
Context: .... Do not add service worker logic. ### TailwindCSS Tailwind v4 uses CSS-based config (`asse...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (5)
app.vue (1)

14-26: Server-side SEO meta block looks good.

useSeoMeta is correctly placed inside the if (import.meta.server) guard, OG image is built as an absolute URL via useRequestURL().origin, and translations are sourced through useI18n(). This aligns with the documented SEO/Head management approach.

As per coding guidelines: "Use useSeoMeta for dynamic global tags that need the request host, placed inside an if (import.meta.server) guard in app.vue" and "Build OG image URLs dynamically with useRequestURL().origin on the server only and ensure they are absolute URLs".

nuxt.config.js (2)

36-44: Root-path special-case in mirroredRouteRules looks correct.

The path === '/' ? '/${locale}' : '/${locale}${path}' branch correctly avoids producing /es/, /ru/, /ja/ for the home route while still mirroring all other rules. Combined with '/' : { headers: cacheHeaders } in pageRouteRules, locale-prefixed home pages now inherit the same cache headers as the default-locale home.


186-214: i18n module config and the documented canonicalQueries workaround read well.

baseUrl points to the production URL (canonical strategy preserved), locales/defaultLocale come from a single source, and the inline comment clearly documents both halves of the SSR/CSR canonicalQueries workaround plus a removal checklist tied to upstream fixes — matches the bug description in pages/posts/[domain].vue and server/plugins/fix-canonical-queries.ts.

pages/posts/[domain].vue (2)

804-817: Client-side canonical override is a clean implementation of the documented two-part fix.

canonicalUrl correctly uses project.urls.production.origin (so clone domains still emit the production canonical), preserves the locale-prefixed route.path, and re-applies the tags query that i18n v10 strips on hydration. The inline comment + cross-reference to server/plugins/fix-canonical-queries.ts makes the workaround easy to retire when upstream fixes canonicalQueries.

As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".


778-796: ⚡ Quick win

The ?? operator correctly triggers the fallback for empty tag selections.

The concern about tagsTitle being an empty string is unfounded. useTagTitle() explicitly returns null when tags.length === 0 (line 17–18 of composables/useTagTitle.ts), so the ?? operator will correctly use the localized "various" fallback. The code is working as intended.

Comment thread AGENTS.md Outdated
Add tests to ensure SEO-related tags are correct. In test/pages/posts.test.ts, add browser tests that verify the canonical link preserves query tags after hydration and updates correctly on client-side tag navigation. In test/server/fix-canonical-queries.test.ts, add a getOgImage helper and tests that ensure og:image is absolute on the home and posts pages and that only a single og:image tag is present.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • AGENTS.md

Commit: 9e1359a79c1d7d07d4fc4b6460b0717a91369bb0

The changes have been pushed to the i18n branch.

Time taken: 2m 35s

Fixed 1 file(s) based on 1 unresolved review comment.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/pages/posts.test.ts`:
- Around line 49-50: The test currently hard-codes the mocked API origin in the
links.self string; change it to derive the base origin from the
NUXT_PUBLIC_API_URL runtime config (or process.env.NUXT_PUBLIC_API_URL with a
default of 'http://localhost:8081') instead of "http://localhost:8081". Update
the construction of links.self (the object key shown as self) to concatenate the
configured base URL with the booru path and query (using the existing pageID
variable), and ensure the test sets or relies on NUXT_PUBLIC_API_URL so it
remains environment-agnostic.
- Around line 166-167: Replace the boolean visibility check that calls
isVisible() inside expect(...).toBe(true) with Playwright's retryable assertion
toBeVisible() to avoid timing flakiness: locate the call using
page.getByRole('button', { name: /1girl/i }).first() and change the assertion so
you await expect(...) and use .toBeVisible() instead of evaluating .isVisible()
and comparing to true; ensure you import/use the Playwright test runner's expect
where appropriate.

In `@test/server/fix-canonical-queries.test.ts`:
- Around line 20-22: The current regex in getOgImage only matches when the
property attribute appears before content; change the matcher to allow
attributes in any order by using attribute-agnostic lookaheads: require a <meta
...> tag that has a property="og:image" (via a lookahead) and a content="..."
(via another lookahead) and capture the content value from the content
lookahead; update the regex used inside getOgImage to use these lookaheads so
reversed attribute order still yields the og:image URL.
- Around line 1-10: Replace the plain await setup() call with a browser-mode
setup: await setup({ browser: true, ...debugBrowserOptions }) and add an import
for debugBrowserOptions at the top of the file (import { debugBrowserOptions }
from 'test/helper'); update references to the setup function call accordingly so
the test suite runs with browser:true and the shared debug options.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7da6d431-7d6c-4a2d-8867-6b73b5f1e71f

📥 Commits

Reviewing files that changed from the base of the PR and between 35aa2d3 and 9e1359a.

📒 Files selected for processing (3)
  • AGENTS.md
  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{vue,ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Import components without path prefix (e.g., <DomainSelector> not <Input/DomainSelector>) — components are auto-imported flat via pathPrefix: false

Files:

  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts
test/**/*.{test,spec}.{ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Use @nuxt/test-utils with Playwright browser mode for tests; call await setup({ browser: true }) inside describe blocks

Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in debug mode

Files:

  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts
**/*.{ts,tsx,js,jsx,vue,css,json}

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates

Files:

  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-06T21:50:37.220Z
Learning: Do not add service worker logic — the service worker is intentionally disabled with `selfDestroying: true`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-06T21:50:37.220Z
Learning: Enable source map uploads to Sentry only in production Docker builds with required `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` build args
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-06T21:50:37.220Z
Learning: Require Node ≥ 24 as specified in `package.json` engines
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-06T21:50:37.220Z
Learning: Clone the repository with `--recursive` flag to initialize the Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-06T21:50:37.220Z
Learning: Configure external API calls to use `NUXT_PUBLIC_API_URL` (default `http://localhost:8081`)
🪛 LanguageTool
AGENTS.md

[grammar] ~95-~95: Ensure spelling is correct
Context: .... Do not add service worker logic. ### TailwindCSS Tailwind v4 uses CSS-based config (`asse...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (1)
AGENTS.md (1)

1-127: LGTM! Comprehensive and accurate project documentation.

The documentation accurately reflects the project architecture, setup requirements, and conventions. All learnings are correctly incorporated (Node ≥24, Git submodule setup, API configuration, disabled service worker, Sentry source map requirements), past review issues have been resolved, and the i18n/SEO guidance matches the changes introduced in this PR.

Note: The LanguageTool hint flagging "TailwindCSS" at line 95 is a false positive—TailwindCSS is the correct spelling of the framework name.

Comment thread test/pages/posts.test.ts Outdated
Comment thread test/pages/posts.test.ts Outdated
Comment thread test/server/fix-canonical-queries.test.ts
Comment thread test/server/fix-canonical-queries.test.ts
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 4 unresolved review comments.

Files modified:

  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts

Commit: e63eeca8bfe217cf4d3ffea7af8810e70dbe6bba

The changes have been pushed to the i18n branch.

Time taken: 7m 11s

Fixed 2 file(s) based on 4 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/pages/posts.test.ts`:
- Around line 395-400: Replace the non-retrying getAttribute-based assertion
with a retryable Playwright expectation: instead of calling
page.locator('link[rel="canonical"]').getAttribute('href') and
expect(...).toContain(...), use the locator and
expect(locator).toHaveAttribute('href', <matcher>) so the check is retried until
the canonical link is present (e.g. use expect.stringContaining('tags=1girl') or
the appropriate string matcher). Update the specific assertion in posts.test.ts
that references page.locator('link[rel="canonical"]') and apply the same change
to the other occurrences noted around lines 408-421.
- Around line 308-312: The polling scroll loop is brittle; replace it by
triggering a scroll once (call page.evaluate(() => window.scrollTo(0,
document.body.scrollHeight))) to kick off pagination and then use Playwright's
page.waitForURL with a predicate to deterministically wait for the paginated URL
(e.g. await page.waitForURL(u => u.includes('tags=hair_bun') &&
!u.includes('page=0'), { timeout: 10000 })); remove the for-loop and
page.url()/waitForTimeout polling and keep only page.evaluate and
page.waitForURL to reliably await the navigation.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 927083f7-b052-4048-93cf-a10ee696e4df

📥 Commits

Reviewing files that changed from the base of the PR and between 9e1359a and e63eeca.

📒 Files selected for processing (2)
  • test/pages/posts.test.ts
  • test/server/fix-canonical-queries.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Use useSeoMeta within an if (import.meta.server) guard for dynamic global SEO tags that need the request host (description, keywords, OG image)

Canonical URLs must point to production (https://r34.app/…) even when served from clone domains - use project.urls.production for canonicals

Files:

  • test/server/fix-canonical-queries.test.ts
  • test/pages/posts.test.ts
**/*.{js,ts,tsx,jsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

Follow Prettier formatting: 120-char print width, no semicolons, single quotes, no trailing commas

Files:

  • test/server/fix-canonical-queries.test.ts
  • test/pages/posts.test.ts
test/**

📄 CodeRabbit inference engine (AGENTS.md)

Tests using @nuxt/test-utils with Playwright must call await setup({ browser: true }) inside describe blocks

Files:

  • test/server/fix-canonical-queries.test.ts
  • test/pages/posts.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

In test mode, route $fetch() calls with baseURL: '' to the local Nitro test server - $test.runtimeConfig.public.apiUrl is set to empty string

Files:

  • test/server/fix-canonical-queries.test.ts
  • test/pages/posts.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-07T07:53:07.094Z
Learning: The service worker is intentionally disabled (`selfDestroy: true`) - do not add service worker logic
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-07T07:53:07.094Z
Learning: Custom scroll behavior skips scroll-to-top when only the `page` query param changes between same-route navigations
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-07T07:53:07.094Z
Learning: Images are deliberately generated at 1x density only in webp format via custom imgproxy provider to reduce bandwidth
Learnt from: CR
Repo: Rule-34/App

Timestamp: 2026-05-07T07:53:07.094Z
Learning: The external API service runs separately at `NUXT_PUBLIC_API_URL` (default `http://localhost:8081`) - see https://github.com/Rule-34/API for API codebase
🔇 Additional comments (1)
test/server/fix-canonical-queries.test.ts (1)

10-10: ⚡ Quick win

No action required — the code is correctly implemented.

debugBrowserOptions is explicitly typed as TestOptions['browserOptions'] in test/helper.ts, containing only type and launch properties. The current usage browserOptions: debugBrowserOptions is the correct pattern and matches the intended shape. The previous suggestion to spread it at the top level would have been incorrect.

			> Likely an incorrect or invalid review comment.

Comment thread test/pages/posts.test.ts Outdated
Comment thread test/pages/posts.test.ts Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 2 unresolved review comments.

Files modified:

  • test/pages/posts.test.ts

Commit: 637747cf29ed16581a7eda97ea38de10970b20bc

The changes have been pushed to the i18n branch.

Time taken: 5m 10s

Fixed 1 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@AlejandroAkbal
Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant