feat: localization#120
Conversation
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.
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds 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. ChangesI18n, Routing, SSR & Tests (single cohort)
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)
Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
|
There was a problem hiding this comment.
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
tparameter infilter()callback shadows theuseI18n()tfunction.The
const { t } = useI18n()added on line 48 is shadowed by the arrow function parametertin thefilter((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 valueLGTM — 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'sfilterConfiglabels are hardcoded in English. Consider i18n-ing thefilterConfigoption 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 winUse the matching label for the menu toggle state.
When
isMenuActiveis 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 winKeep the browser-test harness explicit in this spec.
This file uses
createPage, but it doesn't importdebugBrowserOptions, and thesetupcall hides thebrowser: truerequirement 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-utilswith Playwright by callingawait setup({ browser: true })within describe blocks" and "ImportdebugBrowserOptionsfromtest/helper.tsin 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 | 🟠 MajorHandle non-
FetchErrorfailures before readingresponse.data.The
.catch()block returns any thrown error, but only theFetchErrorbranch returns early. A non-FetchError exception (e.g.,TypeError, network error) will skip the error handling and fall through toresponse.data, which attempts to read a.dataproperty that doesn't exist on plainErrorobjects. 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 | 🟠 MajorPreserve locale in footer navigation links.
The
NuxtLinkcomponents 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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (84)
AGENTS.mdapp.vueassets/js/SeoHelper.tsassets/js/sidebarLinks.tscomponents/PostPageError.vuecomponents/layout/FeedbackButton.vuecomponents/layout/PwaUpdater.vuecomponents/layout/Slideover.vuecomponents/layout/modal/FeedbackPrompt.vuecomponents/layout/modal/NewsletterPrompt.vuecomponents/layout/modal/PremiumPrompt.vuecomponents/layout/modal/PwaPrompt.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/BottomSheetWrapper.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/News.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/SimpleSearch.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/PromotedContent.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/SearchMenu.vuecomponents/pages/posts/navigation/search/SearchMenuWrapper.vuecomponents/pages/posts/navigation/search/SearchSelect.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostChatWithAi.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostDownload.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSave.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/settings/SettingSelect.vuecomponents/shared/PromotionalBanner.vuecomponents/shared/ScrollTopButton.vuecomponents/shared/ShareButton.vuecomponents/shared/ShowMore.vuecomposables/locale.tscomposables/usePageHistory.tscomposables/useTagCollections.tscomposables/useTagTitle.tsconfig/i18n.tsconfig/promotions.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonlayouts/default.vuemiddleware/auth-redirect-if-logged-in.tsmiddleware/auth.tsnuxt.config.jspackage.jsonpages/cookie-policy.vuepages/dmca.vuepages/index.vuepages/legal.vuepages/other-sites.vuepages/posts/[domain].vuepages/premium/additional-boorus.vuepages/premium/backup.vuepages/premium/dashboard.vuepages/premium/forgot-password.vuepages/premium/index.vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/terms-of-service.vueserver/api/_sitemap-urls.tsserver/plugins/fix-canonical-queries.tsserver/plugins/lcp-preload-fetchpriority.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/pages/premium/migrate-old-data.mock-data.tstest/pages/premium/migrate-old-data.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tsvitest.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.vuecomponents/layout/FeedbackButton.vuecomponents/pages/settings/SettingSelect.vuecomponents/pages/home/SimpleSearch.vuecomponents/pages/posts/navigation/search/SearchSelect.vuemiddleware/auth-redirect-if-logged-in.tscomponents/PostPageError.vuecomponents/layout/navigation/BottomSheetWrapper.vuecomponents/layout/modal/NewsletterPrompt.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/shared/ShareButton.vueserver/api/_sitemap-urls.tscomponents/layout/Slideover.vuecomponents/layout/modal/FeedbackPrompt.vuecomponents/layout/navigation/SidebarWrapper.vuepages/premium/forgot-password.vuepages/dmca.vuecomponents/layout/modal/PwaPrompt.vuecomponents/shared/ScrollTopButton.vuevitest.config.tscomponents/pages/posts/post/PostChatWithAi.vuepages/other-sites.vuecomponents/shared/ShowMore.vuecomponents/pages/home/News.vuecomponents/pages/posts/post/PostDownload.vuetest/helper.tspages/posts/[domain].vueconfig/i18n.tslayouts/default.vueserver/plugins/fix-canonical-queries.tscomponents/pages/home/Newsletter.vuepages/premium/backup.vuepages/legal.vuecomposables/useTagTitle.tspages/premium/sign-in.vuetest/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tscomponents/layout/PwaUpdater.vuecomponents/pages/home/PageHistory.vuepages/terms-of-service.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/home/FeaturedTags.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuecomponents/pages/posts/navigation/search/SearchMenu.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/search/SearchMenuWrapper.vuepages/premium/additional-boorus.vuecomposables/usePageHistory.tscomponents/pages/posts/navigation/search/TagCollections.vueassets/js/sidebarLinks.tscomponents/layout/modal/PremiumPrompt.vuecomponents/pages/posts/post/PostTag.vueconfig/promotions.tscomponents/pages/posts/post/PostComponent.vuetest/server-mocks/plugin.tscomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuepages/privacy-policy.vueserver/plugins/lcp-preload-fetchpriority.tscomposables/useTagCollections.tscomponents/shared/PromotionalBanner.vuecomposables/locale.tscomponents/pages/posts/post/PostSaveFallback.vueapp.vuecomponents/pages/posts/PromotedContent.vuepages/premium/dashboard.vuemiddleware/auth.tspages/index.vuepages/premium/index.vuecomponents/pages/posts/PostsPageFooter.vuetest/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.vuecomponents/layout/FeedbackButton.vuecomponents/pages/settings/SettingSelect.vuecomponents/pages/home/SimpleSearch.vuecomponents/pages/posts/navigation/search/SearchSelect.vuemiddleware/auth-redirect-if-logged-in.tscomponents/PostPageError.vuecomponents/layout/navigation/BottomSheetWrapper.vuecomponents/layout/modal/NewsletterPrompt.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/shared/ShareButton.vueserver/api/_sitemap-urls.tscomponents/layout/Slideover.vuecomponents/layout/modal/FeedbackPrompt.vuecomponents/layout/navigation/SidebarWrapper.vuepages/premium/forgot-password.vuepages/dmca.vuecomponents/layout/modal/PwaPrompt.vuecomponents/shared/ScrollTopButton.vuevitest.config.tscomponents/pages/posts/post/PostChatWithAi.vuepages/other-sites.vuecomponents/shared/ShowMore.vuecomponents/pages/home/News.vuecomponents/pages/posts/post/PostDownload.vuetest/helper.tspages/posts/[domain].vueconfig/i18n.tslayouts/default.vueserver/plugins/fix-canonical-queries.tscomponents/pages/home/Newsletter.vuepages/premium/backup.vuepages/legal.vuecomposables/useTagTitle.tspages/premium/sign-in.vuetest/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tscomponents/layout/PwaUpdater.vuecomponents/pages/home/PageHistory.vuepages/terms-of-service.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/home/FeaturedTags.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuecomponents/pages/posts/navigation/search/SearchMenu.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/search/SearchMenuWrapper.vuepages/premium/additional-boorus.vuecomposables/usePageHistory.tscomponents/pages/posts/navigation/search/TagCollections.vueassets/js/sidebarLinks.tscomponents/layout/modal/PremiumPrompt.vuecomponents/pages/posts/post/PostTag.vueconfig/promotions.tscomponents/pages/posts/post/PostComponent.vuetest/server-mocks/plugin.tscomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuepages/privacy-policy.vueserver/plugins/lcp-preload-fetchpriority.tscomposables/useTagCollections.tscomponents/shared/PromotionalBanner.vuecomposables/locale.tscomponents/pages/posts/post/PostSaveFallback.vueapp.vuecomponents/pages/posts/PromotedContent.vuepages/premium/dashboard.vuemiddleware/auth.tspages/index.vuepages/premium/index.vuecomponents/pages/posts/PostsPageFooter.vuenuxt.config.jstest/pages/posts.test.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use the
qslibrary for query string parsing instead of standard URLSearchParams
Files:
middleware/auth-redirect-if-logged-in.tsserver/api/_sitemap-urls.tsvitest.config.tstest/helper.tsconfig/i18n.tsserver/plugins/fix-canonical-queries.tscomposables/useTagTitle.tstest/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tscomposables/usePageHistory.tsassets/js/sidebarLinks.tsconfig/promotions.tstest/server-mocks/plugin.tsserver/plugins/lcp-preload-fetchpriority.tscomposables/useTagCollections.tscomposables/locale.tsmiddleware/auth.tsnuxt.config.jstest/pages/posts.test.ts
vitest.config.ts
📄 CodeRabbit inference engine (AGENTS.md)
In test mode, set
$test.runtimeConfig.public.apiUrlto an empty string so$fetch(baseURL: '')routes to the local Nitro test serverDisable Sentry source map uploads in tests by clearing environment variables in
vitest.config.tsTests run serially without file parallelism
Files:
vitest.config.ts
config/i18n.ts
📄 CodeRabbit inference engine (AGENTS.md)
Define locales in
config/i18n.tsas 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.tsworkaround 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-utilswith Playwright for tests by callingawait setup({ browser: true })within describe blocksImport
debugBrowserOptionsfromtest/helper.tsin test files to enable headful playback with slowMo for debugging
Files:
test/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tstest/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.tsinjected 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
capitalizefrom 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 theconsole.infocalls inimport.meta.clientcorrectly prevents SSR-side execution.composables/useTagCollections.ts (1)
6-21: LGTM! MovingdefaultTagCollectionsinside the composable to consumeuseI18n()is the correct pattern.One minor note: the collection
namestrings 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/catchis 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 todefaultBrowserOptions(headless) as the default is correct.
debugBrowserOptionsremains exported and reachable for individual test debugging viapage.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
prefixedLocaleCodesandlocaleCodesfrom the samelocalesarray keeps routing and runtime checks aligned. As per coding guidelines, "Define locales inconfig/i18n.tsas the single source of truth for i18n configuration".composables/locale.ts (1)
4-15: Path normalization looks solid.Sharing locale detection with
config/i18n.tsand 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-labelkeeps 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;
$tis available globally from Nuxt i18n without an explicituseI18n()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 —localePathcorrectly wraps internal routes for locale-aware navigation.pages/terms-of-service.vue (1)
6-9: LGTM —computed()wrappers inuseSeoMetacorrectly 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+baseURLpattern used for other booru API calls.pages/cookie-policy.vue (1)
4-15: LGTM — correct reactive i18n pattern foruseSeoMeta.components/shared/ShareButton.vue (1)
12-63: LGTM — clean i18n integration for toast messages andaria-label.components/pages/posts/navigation/search/SearchMenu.vue (1)
35-393: LGTM — consistentt()usage throughout the template.components/layout/PwaUpdater.vue (1)
17-30: LGTM — correct template-only$t()usage without needinguseI18n()in script.components/layout/modal/PwaPrompt.vue (1)
28-67: LGTM — correct$t()usage; good to see theiframetitleattribute 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 pullinguseI18n()into the script block is the correct approach here.components/layout/navigation/SidebarWrapper.vue (1)
1-110: LGTM – usinglink.nameKey(the stable translation key string) as thev-for:keyis the right choice for locale switching;localePathcorrectly 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 – wrappingnewsListincomputed()is the right approach to keep translations reactive on locale change.pages/privacy-policy.vue (1)
4-10: LGTM –useI18n(),useLocalePath(), and all threelocalePath('/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'slocalePathusesrouter.resolve()internally which parses query params from path strings. ThenavigateTo(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:
localePathis used for navigation and link hrefs,computed(() => t(...))ensures reactive SEO meta updates on locale change, andnavigateTouses 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
localePathternary 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
tagTypeLabelscomputed correctly makes tag-category headings reactive to locale changes. ThecreateTaghelper cleanly centralizesTagconstruction. ThepostSourceFileUrlcomputed correctly falls back to the video URL when no poster exists, and using the poster forPostSourceon 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. ThelocalePathcalls with embedded query/hash strings increateVideoPlayerare 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 — theserviceName/actionrefactor andgetServiceTitlehelper 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 tolocalePath()are internal paths, and thecomputed(() => t(...))SEO meta pattern is correct.components/pages/posts/navigation/search/TagCollections.vue (1)
36-61: LGTM — toast/prompt strings are correctly localized and theTagCollectioncreation flow is unchanged.assets/js/sidebarLinks.ts (1)
12-62: All consumers ofsidebarNavigationare properly using.nameKey.Both
Sidebar.vueandSidebarWrapper.vuecorrectly accessnameKeyfor keys and translations. The.namereferences found inpremium/dashboard.vueandpremium/index.vueare on separate local data structures, not onsidebarNavigation.
| 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') |
There was a problem hiding this comment.
🧹 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 -50Repository: 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 jsRepository: Rule-34/App
Length of output: 83
🏁 Script executed:
# Check the test file to see full context
cat test/pages/posts.test.tsRepository: Rule-34/App
Length of output: 12783
🏁 Script executed:
# Search for URLSearchParams usage across test files
rg "URLSearchParams" --type ts --type tsx --type jsRepository: 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 jsRepository: Rule-34/App
Length of output: 97
🏁 Script executed:
# Search for URLSearchParams usage in the codebase
rg "URLSearchParams" --type ts --type js -B2 -A2Repository: 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.tsRepository: 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 -A3Repository: Rule-34/App
Length of output: 6858
🏁 Script executed:
# Check usePageHistory.ts to understand why it uses URLSearchParams
head -50 composables/usePageHistory.tsRepository: 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 -A2Repository: 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.
| 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.
| 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') |
There was a problem hiding this comment.
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.
| 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').
| export default defineConfig({ | ||
| test: { | ||
| testTimeout: 15000 | ||
| } | ||
| }) |
There was a problem hiding this comment.
Missing required test-config entries per coding guidelines.
Two required settings are absent from the test block:
- Serial execution — without
fileParallelism: false, test files can run concurrently, which can cause port conflicts or race conditions against the local Nitro dev server. - Local API routing — the coding guideline requires setting
$test.runtimeConfig.public.apiUrlto''(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).
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.
Regarding the
|
…le, backup test, hygiene
There was a problem hiding this comment.
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 valueMinor inconsistency: mixing
t()and$t()in template.The script uses
t()(destructured fromuseI18n()), while the template mixes botht('...')and$t('...')(e.g., line 902 uses$t, line 954 uses$t, while most script-bound expressions uset). 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 winHandle malformed backup JSON inside the restore error path.
A bad
.jsonfile throws atJSON.parse(...)before thetry/catch, so the user gets neithererrors.backupVersionNotSupportednortoasts.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 winRestore a real backup artifact assertion.
__backupDateFormattingCalls > 0only 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
📒 Files selected for processing (27)
AGENTS.mdcomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/PromotedContent.vuecomponents/pages/posts/post/PostSave.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/PromotionalBanner.vuecomposables/useTagTitle.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonjest.config.jsnuxt.config.jspackage.jsonpages/index.vuepages/posts/[domain].vuepages/premium/backup.vuepages/premium/saved-posts/[domain].vuepages/settings.vueserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/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 topathPrefix: falseconfiguration in Nuxt
Files:
server/api/_sitemap-urls.tscomponents/pages/posts/post/PostSave.vuetest/server-mocks/plugin.tscomponents/pages/posts/PromotedContent.vuetest/server/fix-canonical-queries.test.tscomponents/pages/home/FeaturedTags.vueserver/plugins/lcp-preload-fetchpriority.tscomponents/shared/PromotionalBanner.vuecomposables/useTagTitle.tspages/settings.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/navigation/Navbar.vuepages/index.vuetest/pages/premium/backup.test.tspages/premium/saved-posts/[domain].vuecomponents/layout/navigation/Sidebar.vuepages/premium/backup.vuepages/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.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tsserver/plugins/lcp-preload-fetchpriority.tscomposables/useTagTitle.tstest/pages/premium/backup.test.tsnuxt.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.tscomponents/pages/posts/post/PostSave.vuetest/server-mocks/plugin.tscomponents/pages/posts/PromotedContent.vuetest/server/fix-canonical-queries.test.tscomponents/pages/home/FeaturedTags.vueserver/plugins/lcp-preload-fetchpriority.tscomponents/shared/PromotionalBanner.vuecomposables/useTagTitle.tspages/settings.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/navigation/Navbar.vuepages/index.vuetest/pages/premium/backup.test.tspages/premium/saved-posts/[domain].vuecomponents/layout/navigation/Sidebar.vuepages/premium/backup.vuenuxt.config.jspages/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.tscomponents/pages/posts/post/PostSave.vuetest/server-mocks/plugin.tscomponents/pages/posts/PromotedContent.vuetest/server/fix-canonical-queries.test.tscomponents/pages/home/FeaturedTags.vueserver/plugins/lcp-preload-fetchpriority.tscomponents/shared/PromotionalBanner.vuecomposables/useTagTitle.tspages/settings.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/navigation/Navbar.vuei18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ru.jsonpackage.jsonpages/index.vuetest/pages/premium/backup.test.tspages/premium/saved-posts/[domain].vuei18n/locales/ja.jsoncomponents/layout/navigation/Sidebar.vuepages/premium/backup.vuenuxt.config.jspages/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.vuecomponents/pages/posts/PromotedContent.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/PromotionalBanner.vuepages/settings.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/navigation/Navbar.vuepages/index.vuepages/premium/saved-posts/[domain].vuecomponents/layout/navigation/Sidebar.vuepages/premium/backup.vuepages/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.tsinjected throughnuxt.config.js
Files:
test/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/pages/premium/backup.test.ts
**/test/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright browser mode insidedescribeblocks that callawait setup({ browser: true })Import
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo during test debugging
Files:
test/server/fix-canonical-queries.test.tstest/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.jsoni18n/locales/es.jsoni18n/locales/ru.jsoni18n/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 routesUse custom
imgproxyprovider for<NuxtImg>components and generate images at 1x density only in webp format to reduce bandwidthIn test mode, set
$test.runtimeConfig.public.apiUrlto empty string so$fetch(baseURL: '')routes to local Nitro test serverFully disable Sentry in tests via
$test.sentry.enabled: falseinnuxt.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 —jestremoved,@nuxtjs/i18nand@nuxt/test-utilscorrectly added.The devDependency cleanup (removing unused
jestin favour ofvitest) and the new@nuxtjs/i18nproduction 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 — conditionalaria-labelcorrectly reflects save/unsave state.Both
common.savePostandcommon.unsavePostkeys 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 —localePathcorrectly guarded behindisExternalHref,undefinedused for non-external attrs.Both issues from the previous review (locale-prefixing external URLs and dangling
rel/targetattributes) are resolved. Thealttext is properly localized.components/pages/posts/post/PostTag.vue (1)
48-79: LGTM — i18n wiring is correct in both script and template.
t()fromuseI18n()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 —localePathnow correctly guarded against external promo URLs.The conditional
isExternalHrefcheck onhref,target, andreladdresses 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, andgetRouteBaseName(route)strips locale suffixes from route names — consistent with the same pattern incomposables/usePageHistory.ts. Thev-intersection-observersimplification (handler directly, no array wrapper) is valid per@vueuse/componentsAPI.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. TheseoHelperconnector strings (lines 538-540) properly support the locale-aware title building inuseTagTitle.composables/useTagTitle.ts (1)
26-28: The emptycleanedTagsguard was correctly added.This addresses the previously flagged issue where
buildTitlecould return''instead ofnullwhen all tag names normalized tonull.pages/settings.vue (1)
55-64: Label-to-value reverse mapping is correct but fragile.The
onSelectedListChangefunction 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:filterConfigis nowcomputedfor locale reactivity.Making
filterConfiga 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 withbaseURLcorrectly.The refactored fetch uses a relative endpoint (
/booru/${...}/tags) withbaseURL: 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 usinguseI18n()insidedefinePageMeta.validateis 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:filterConfigis 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: trueenables locale-aware sitemap URLs.The switch from
_sitemap: 'pages'to_i18nTransform: truecorrectly integrates with@nuxtjs/sitemap+@nuxtjs/i18nto 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/**/postsand/booru/**/tagsendpoints for SSR tests. The comment on lines 20-21 explicitly documents the 404 behavior for unmatched paths, addressing the previous review feedback.
| 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) | ||
| }) |
There was a problem hiding this comment.
🧹 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.
…en security, backup JSON.parse, hasTags, locale escapes, nitpicks
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
composables/useTagTitle.ts (1)
42-45:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove the extra separator before
seoHelper.without.
seoHelper.withoutalready includes surrounding spaces in the locale dictionaries, soif (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 winDerive
hasTagsfrom the filtered tag list, not from the fallback string.Comparing
formattedTags[0]tot('seoFooter.defaultTag')misclassifies real searches liketags=animeas “no tags”, because the explicit tag normalizes to the same text as the fallback. Build one filtered-and-normalized array (dropping nulls fromnormalizeStringForTitle) and use its length for bothhasTagsand 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 winTranslate the sort value before inserting it into localized copy.
props.selectedFilters.sortis still the raw query enum here, so the footer can render internal values likescoreinside translated paragraphs. Map it through the existingfilters.sortBy*labels first, the same wayratingLabelalready 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
📒 Files selected for processing (9)
components/pages/posts/PostsPageFooter.vuecomposables/useTagTitle.tsi18n/locales/es.jsoni18n/locales/ru.jsonnuxt.config.jspages/index.vuepages/premium/backup.vueserver/plugins/lcp-preload-fetchpriority.tstest/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 viapathPrefix: falsein nuxt.config.jsFor images via
<NuxtImg>, use the custom imgproxy provider; deliberately generate images at 1x density only in webp format to reduce bandwidth
Files:
composables/useTagTitle.tstest/server/fix-canonical-queries.test.tsserver/plugins/lcp-preload-fetchpriority.tscomponents/pages/posts/PostsPageFooter.vuepages/index.vuepages/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.tstest/server/fix-canonical-queries.test.tsserver/plugins/lcp-preload-fetchpriority.tscomponents/pages/posts/PostsPageFooter.vuepages/index.vuepages/premium/backup.vuei18n/locales/es.jsoni18n/locales/ru.jsonnuxt.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.tsinjected throughnuxt.config.js; set$test.runtimeConfig.public.apiUrlto empty string so$fetchroutes 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-utilsand Playwright, callawait setup({ browser: true })insidedescribeblocksFor debug mode in browser tests, import
debugBrowserOptionsfromtest/helper.tsfor 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
…ion, hreflang regex, rate-limit URL, useTagTitle spacing
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (1)
test/server/fix-canonical-queries.test.ts (1)
54-61: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueHreflang regex robustness — past comment now addressed; consider tightening the assertion.
The
alternateTagsandcodesregexes 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
📒 Files selected for processing (4)
components/pages/posts/PostsPageFooter.vuecomposables/useTagTitle.tspages/index.vuetest/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.tscomposables/useTagTitle.tscomponents/pages/posts/PostsPageFooter.vuepages/index.vue
test/**/*.test.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright insidedescribeblocks that callawait setup({ browser: true })For browser test debugging, import
debugBrowserOptionsfromtest/helper.tsfor 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
hasTagsfromincludedTagsand defaultingformattedTagsavoids 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 assumesconfig.public.apiUrlis absolute. If this is configured as a path such as/booru, theURL()constructor can throw and the toast action stops working. Resolve the API base againstwindow.location.originbefore 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') + } }
…, explicit spacing, params->query, hreflang assertion
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Fixed 2 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
i18n/locales/en.jsoni18n/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.jsoni18n/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 winThe
{code}placeholder is properly handled. The Vue componentpages/premium/tag-collections.vueuses 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.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
i18n/locales/en.jsonnuxt.config.jspages/posts/[domain].vueserver/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.tspages/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.tsi18n/locales/en.jsonnuxt.config.jspages/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.tspages/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.tsplugin to patch the canonical<link>in SSR output sincecanonicalQueriesin 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()helperIn test mode, use
$test.runtimeConfig.public.apiUrlset to''so$fetch(baseURL: '')routes to the local Nitro test serverDisable Sentry in tests via
$test.sentry.enabled: falseinnuxt.config.jsThe custom scroll behavior should skip scroll-to-top when only the
pagequery param changes between same-route navigations
Files:
nuxt.config.js
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use custom
imgproxyprovider 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
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 4 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
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 winDeclare
testAuthBypassin the baseruntimeConfig.public.
testAuthBypassis only introduced under$test.runtimeConfig.public(Line 428) and is not declared in the baseruntimeConfig.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 byNUXT_PUBLIC_*env overrides, and is consistent across environments. Add a default offalse(orundefined) 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
📒 Files selected for processing (2)
nuxt.config.jspages/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.jspages/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.jspages/posts/[domain].vue
**/*.{css,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use TailwindCSS v4 via
@tailwindcss/viteVite 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, andpageRouteRulesnow includes/so localized home pages inherit the samecacheHeaders. This correctly resolves the prior concern.
73-75: Conditional preconnect emission looks good.The preconnect entry is now only appended when
NUXT_PUBLIC_API_URLis set, avoiding meaningless head items in local/test setups.
85-85:og:imageis 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 valueNo action required — the
experimental.strictSeoobject shape is valid in@nuxtjs/i18nv10.The type signature correctly supports
boolean | SeoAttributesOptions, and the configuration as written won't trigger type errors. The existing inline comments adequately document thatcanonicalQueriesis ignored at runtime and reference the SSR/CSR workarounds in place.
141-150: ⚡ Quick winThis review concern is based on incorrect assumptions about Nuxt 4's config merging behavior.
Nuxt 4 concatenates (appends) array values in
$testoverrides rather than replacing them. Both~/server/plugins/lcp-preload-fetchpriorityand~/test/server-mocks/plugin.tswill 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 winNo issue found —
buildTagTitlereturnsnullfor empty tags, not an empty string.The
useTagTitlecomposable explicitly returnsnullwhen the tags array is empty or after normalization yields no tags (see thereturn nullconditions and|| nullat the end). The nullish-coalescing operator on Line 765 therefore correctly falls back tot('posts.seo.descriptionVarious')when no tags are selected. The code is correct as written.> Likely an incorrect or invalid review comment.
767-769: ⚡ Quick winThe code is correct as written. The
descriptionScoreOfkey 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 winThis is a documented temporary workaround; verify the two-part fix is properly scoped before ship.
The two concerns are worth checking:
Canonical only carries
tags. Confirmed intentional —page,filter.rating,filter.sort,filter.scoreare intentionally dropped per thecanonicalQueries: ['tags']config innuxt.config.js. Verify this SEO posture (every page in a tag-search shares the same canonical) aligns with your indexing strategy.Duplicate canonical is not a risk in this implementation. The codebase does not use
useLocaleHead; it callsuseHeaddirectly 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/i18nv10 bug wherecanonicalQueriesis non-functional. A removal checklist is in place for when upstream fixes the issue.Confirm
baseUrlis correctly configured innuxt.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.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (4)
AGENTS.mdapp.vuenuxt.config.jspages/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.jspages/posts/[domain].vueapp.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 localePlace static global SEO tags (favicon, rating, monetization, color-scheme) in
nuxt.config.jsunderhead.metaIn test mode, set
$test.runtimeConfig.public.apiUrlto an empty string so$fetch(baseURL: '')routes to the local Nitro test serverDisable Sentry fully in tests by setting
$test.sentry.enabled: falseinnuxt.config.jsImplement custom scroll behavior that skips scroll-to-top when only the
pagequery 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 withpathPrefix: falseBuild OG image URLs dynamically with
useRequestURL().originon the server only and ensure they are absolute URLs, since Open Graph requires absolute URLsUse
project.urls.productionfor canonical URLs to point to production even when served from clone domains, as canonicals prevent duplicate content for SEOUse the
imgproxycustom provider for<NuxtImg>components and generate images at 1x density only in webp format
Files:
pages/posts/[domain].vueapp.vue
**/pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
For page-specific tags (title, description), use
useSeoMetain the page component
Files:
pages/posts/[domain].vue
**/app.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use
useSeoMetafor dynamic global tags that need the request host, placed inside anif (import.meta.server)guard inapp.vue, not innuxt.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.
useSeoMetais correctly placed inside theif (import.meta.server)guard, OG image is built as an absolute URL viauseRequestURL().origin, and translations are sourced throughuseI18n(). This aligns with the documented SEO/Head management approach.As per coding guidelines: "Use
useSeoMetafor dynamic global tags that need the request host, placed inside anif (import.meta.server)guard inapp.vue" and "Build OG image URLs dynamically withuseRequestURL().originon the server only and ensure they are absolute URLs".nuxt.config.js (2)
36-44: Root-path special-case inmirroredRouteRuleslooks 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 }inpageRouteRules, 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.
baseUrlpoints to the production URL (canonical strategy preserved),locales/defaultLocalecome 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 inpages/posts/[domain].vueandserver/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.
canonicalUrlcorrectly usesproject.urls.production.origin(so clone domains still emit the production canonical), preserves the locale-prefixedroute.path, and re-applies thetagsquery that i18n v10 strips on hydration. The inline comment + cross-reference toserver/plugins/fix-canonical-queries.tsmakes the workaround easy to retire when upstream fixescanonicalQueries.As per coding guidelines: "Use
project.urls.productionfor canonical URLs to point to production even when served from clone domains".
778-796: ⚡ Quick winThe
??operator correctly triggers the fallback for empty tag selections.The concern about
tagsTitlebeing an empty string is unfounded.useTagTitle()explicitly returnsnullwhentags.length === 0(line 17–18 ofcomposables/useTagTitle.ts), so the??operator will correctly use the localized "various" fallback. The code is working as intended.
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.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
AGENTS.mdtest/pages/posts.test.tstest/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 viapathPrefix: false
Files:
test/pages/posts.test.tstest/server/fix-canonical-queries.test.ts
test/**/*.{test,spec}.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright browser mode for tests; callawait setup({ browser: true })inside describe blocksImport
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo in debug mode
Files:
test/pages/posts.test.tstest/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.tstest/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.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 2 file(s) based on 4 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 2 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
test/pages/posts.test.tstest/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
useSeoMetawithin anif (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 - useproject.urls.productionfor canonicals
Files:
test/server/fix-canonical-queries.test.tstest/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.tstest/pages/posts.test.ts
test/**
📄 CodeRabbit inference engine (AGENTS.md)
Tests using
@nuxt/test-utilswith Playwright must callawait setup({ browser: true })insidedescribeblocks
Files:
test/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
In test mode, route
$fetch()calls withbaseURL: ''to the local Nitro test server -$test.runtimeConfig.public.apiUrlis set to empty string
Files:
test/server/fix-canonical-queries.test.tstest/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 winNo action required — the code is correctly implemented.
debugBrowserOptionsis explicitly typed asTestOptions['browserOptions']intest/helper.ts, containing onlytypeandlaunchproperties. The current usagebrowserOptions: debugBrowserOptionsis 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.
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 2 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Documentation
Tests