perf: optimize routing, media, and SEO - #121
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📜 Recent review details🧰 Additional context used📓 Path-based instructions (7)**/*.{vue,ts,tsx}📄 CodeRabbit inference engine (AGENTS.md)
Files:
pages/**/*.vue📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{ts,tsx,vue}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.vue📄 CodeRabbit inference engine (AGENTS.md)
Files:
{plugins/**/*,pages/**/*.vue}📄 CodeRabbit inference engine (AGENTS.md)
Files:
**/*.{js,ts,tsx,vue,json,css}📄 CodeRabbit inference engine (AGENTS.md)
Files:
{nuxt.config.{js,ts},**/*.vue}📄 CodeRabbit inference engine (AGENTS.md)
Files:
🧠 Learnings (1)📓 Common learnings🔇 Additional comments (3)
📝 Walkthrough<review_stack_artifact_start> Single cohesive cohort covering build tooling, router/query behavior, composables/utilities, media/player handling, UI and navigation refactors, SEO/schema and i18n expansions, page additions, and test updates.Comprehensive checkpoint covering package/tooling migration to pnpm, router/query and canonical updates, new composables (lazy toast, idle task, favicon), PostMedia/player refactor, UI/navigation/dialog changes, i18n additions and pages, plus test and mock updates.range_858b59f2d53a range_e368b696c823 range_15f9fb1af7c9 range_5e35dc013082 range_d007722f6832 range_88d438d209fc range_91586ae2e469 range_65694c7b074e range_7182bfb111c6 range_bad6719e464a range_de1d60ba0ab8 range_3c13cf7cdb6c range_7d12d20c8a8e range_a3ef36f4f459 range_5eaa65e9b5e4 range_0e20bc5ab46e range_e5aa1f30597d range_4f488eb5fb1d range_9591ab720fe1 range_e8cfcbcb3cd4 range_7ff0aab0e47f range_aa7e440674fa range_d04163068326 range_bbf797365371 range_341d97cbdbb2 range_70462613aa76 range_0f5db0124901 range_81f663c3b66a range_1f547b43da3c range_ea633a9500c6 range_b8f63437b3b4 range_b78252cbed93 range_3608bde0cd1a range_4bdcf83e786c range_680ca04c50ab range_fb9e91538685 range_a80dce5b5d81 range_0dfa2a76e2d3 range_17eefc40de64 range_c731bcdb62c7 range_676004b650aa range_e5c262882ce4 range_d574ca86151e range_a88ed353f85b range_7b3ea76b9218 range_029e04bb8ab8 range_d781e954064f range_cb7ac4ac9cd9 range_c1f67857cb5e range_8dacbf2928c5 range_510c992f14e6 range_bc4fe8ae1df0 range_1cd6a7f7fe8a range_c59fd1880de2 range_9dec86a45ae3 range_6a31f7924178 range_61b0cabc668b range_19a4661c9f2f range_9fbbb7153067 range_0324675f3618 range_247b6396609e range_e90f8569b857 range_edc7cf9e455e range_a3d65b265ce1 range_abe10f8b9119 range_4b2d6ddfb3dd range_621fcd807303 range_70e3b3ea42cb range_4286364248f4 range_8c437bc7104a range_b88c97ab7c62 range_b30b1ead2a16 range_d5a0e04ce047 range_517758899f51 range_f38bf79779b0 range_3b51bcb2569d range_c6bd38106513 range_0342864436af range_7eb839f216ca range_0ba9e770178a range_0a19dd765772 range_6683537a9815 range_86189ad0813e range_11002f00598a range_a9a6009f3fe0 range_515471e96e09 range_36670e2ca594 range_0e4e8946fc92 range_f42d33b2ca0f range_61e80546c29d range_42178b3d94dd range_af1ac3822116 range_789828158415 range_c962ea0cb8d8 range_d9b9a38b7ec3 range_968e14c25a64 range_7c5c6881924a range_1fc90695e387 range_ecdee57f049e range_89f44a1b2ba6 range_ed61e2428ccb range_859b7488c65b range_e46acd0f93dd range_fd3b278b6396 range_794de6f9572d range_6dc29c556ab0 range_0da3833cc725 range_0f8ed311c5b6 range_762d331ffd41 range_1f2bc5933665 range_89e16962917f range_534b42ae26dc range_05aa4ce245b7 range_fbccbc66e6fe range_6ddc7b37ab9e range_505e9b0a76c7 range_ce171b807100 range_465c46a6ad48 range_75183accac62 range_3cafc57ea356 range_1bafb6f22f39 range_d4921f7f5ab0 range_cac033b9b68b range_43a43020e278 range_83f7c604567b range_44d7def76771 range_7922e630bf68 range_a72d198cb089 range_a65edf8156df range_6731c87b682b range_2bdd741eee2f range_3f74d9828a36 range_c5793ce64e98 range_83bfb0ef7fc3 range_d1b3a0ebbc95 range_7002dd4ae19e range_40cbc2d0f075 range_46756f8c3e13 range_2f74ec6b0e95 range_117a365ed5b8 range_9426df8c941f range_1f008ec2367d range_000817b806c7 range_4ae07f1b1012 range_9da3a3e45bb8 range_42c4181d9bd3 range_96a4cc5fc596 range_0d05e5d2b4ca range_8622123cf9d7 range_c4bb25dd0827 range_376a5623f434 range_8387e6235467 range_aec13a4ba4cc range_936d73cd28f3 range_e8ba0b810a9a range_14c9edc5f29f range_d34dc0965913 range_23468deadd0f range_1dca097be2e2 range_3adf51dfb1e0 range_1acbd436a1eb range_2d8a7129635b range_d1eadf1f1f7b range_5820edfc5d2a range_cbe2ff2f3b5c range_98646e57c963 range_c91e99da9e5b range_8f2a1888d3d0 range_acac09aa048d range_3908173dc3a1 range_760d9f65abee range_0a306e21a807 range_47829eecf6c2 range_a092970c5140 range_13697fe5f9e4 range_fd1d3a74e5bb range_35e3e1bb27d7 range_331844c236c0 range_da50a7ee066e range_d3066bc21fb9 range_e669f6c01289 range_25257bc48253 range_42f82839d4bb range_ed7a85d236ca range_8f7ef0d794af range_ecc9827519bc range_746ff502362c range_5bce9513d000 range_d5a02d83b579 range_a3c8826138a6 range_5241dd99c50e range_19955141594b range_c1b41d64814e range_0a3540a5278d range_fdb4aed2e798 range_930f2715e5b5 range_4e9ec898220e range_d8f30a8deaa8 range_88f5d867a335 range_282b91a8fdad range_c96e76824e8b range_1469f5ea1ebc range_111b04c031ff range_18d9a42b0ade range_c8b1d4b006d3 range_07d5941450b3 range_04a3c716e055 range_26ea77f9f362 range_593661287754 range_790487c7f487 range_de14e1fb44fa range_f9faf49c54ac range_bbd183d9b9c3 range_18cd41f7ab94 range_5a93307540eb range_6effb15af201 range_554ac362fd56 range_e862b4917088 range_9ce54cecd778 range_409e4900549a range_77c994b6a936 range_8a1352708f37 range_3f08517b8b52 range_c676226e6de6 range_d6a69949c24e range_2668db0c737c range_c1eb4aa8b580 range_1ec30aec53b9 range_5d6146994f8a range_84611a18bdeb range_f2b1fd5d6c79 range_4587a2428eeb range_6aa41579a1b9 range_29503adbfa04 range_3317d8c843b8 range_9d98560d4fa5 range_f6f245c45c5b range_0897b5937159 range_d5d66544783d range_54a6995ce046 range_113535e9f7fd range_c7d56de85d73 range_bd27978e69c8 range_5b3edd5369f9 range_d297499c06b2 range_e8b2cee32db2 range_8a77676b9501 range_30b093cb6922 range_857fe2df5d90 range_d45425e88962 range_868976b45070 range_8fe95f9a895b range_94cd00122c74 range_bd848d49d8ab range_a8eb9432a16a range_1510256350b3 range_3442ba85e7f4 range_5b6a073c51fa range_ef7daebfde92 range_cd2104aa578c range_f0d40d966eee range_6f02373e0391 range_2ad687405a82 range_f23a0f37bd31 range_83d8cea483c0 range_51890eb9a36d range_4757ab7e296a range_d9d972c113dd range_80b0fbe4333d range_2eab24030dcc range_6689b5588c55 range_5139a2bd3dc0✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
|
left a comment
There was a problem hiding this comment.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
pages/premium/saved-posts/[domain].vue (1)
90-98:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle
route.query.tagsarrays before splitting.
route.query.tagscan bestring[]; calling.split('|')on an array will throw. Normalize first, then split safely.Suggested fix
- const selectedTags = computed(() => { - const tags = route.query.tags as string + const selectedTags = computed(() => { + const rawTags = route.query.tags + const tags = Array.isArray(rawTags) ? rawTags[0] : rawTags if (!tags) { return [] } return tags.split('|').map((tag) => new Tag({ name: tag }).toJSON()) })🤖 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 `@pages/premium/saved-posts/`[domain].vue around lines 90 - 98, The computed selectedTags currently assumes route.query.tags is a string and calls .split('|'), which will throw if it's a string[]; update selectedTags to first normalize tags: const raw = route.query.tags as string | string[] | undefined; if (!raw) return []; const tagList = Array.isArray(raw) ? raw : raw.split('|'); then map tagList with new Tag({ name: tag }).toJSON(); this uses the selectedTags symbol and route.query.tags and avoids calling .split on an array.pages/posts/[domain].vue (1)
824-834:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPreserve
pageand flat filter params in the hydration canonical.This client-side canonical only keeps
tags, so paginated and filtered URLs hydrate back to the base or tags-only canonical. That reintroduces duplicate-canonical drift for result pages whose content changes withpage/filter[...].Suggested fix
const canonicalUrl = computed(() => { const base = `${project.urls.production.origin}${route.path}` - const tags = getSingleQueryValue(route.query.tags) - - if (!tags) return base - return `${base}?tags=${encodeURIComponent(tags)}` + const params = new URLSearchParams() + const tags = getSingleQueryValue(route.query.tags) + const page = getSingleQueryValue(route.query.page) + const rating = getFilterQueryValue(route.query, 'rating') + const sort = getFilterQueryValue(route.query, 'sort') + const score = getFilterQueryValue(route.query, 'score') + + if (tags) params.set('tags', tags) + if (page) params.set('page', page) + if (rating) params.set('filter[rating]', rating) + if (sort) params.set('filter[sort]', sort) + if (score) params.set('filter[score]', score) + + const query = params.toString() + return query ? `${base}?${query}` : base })As per coding guidelines, "use server/plugins/fix-canonical-queries.ts to patch canonical links in SSR HTML; use useHead with useSeoMeta in page components to re-apply canonical after i18n hydration."
🤖 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 `@pages/posts/`[domain].vue around lines 824 - 834, The client-side canonicalUrl computed currently only retains the tags query (computed named canonicalUrl using getSingleQueryValue and route.query) causing paginated and flat filter params (e.g. page and filter[...] keys) to be lost during hydration; update the canonicalUrl computation to include the existing page and any flat filter[...] params from route.query (preserve project.urls.production.origin and route.path), encoding them into the query string alongside tags, and then re-apply via useHead as before; also ensure SSR patching remains via server/plugins/fix-canonical-queries.ts and follow the guideline to re-apply canonical after i18n hydration by using useSeoMeta in the page component so canonical reflects page and filter[...] on both SSR and client hydration.
🤖 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 `@assets/js/sidebarLinkItems.ts`:
- Around line 15-24: The module-level standalone detection in
assets/js/sidebarLinkItems.ts causes SSR/client mismatch; remove the
import.meta.client && window.matchMedia('(display-mode: standalone)').matches
conditional from the exported link list and instead export the full "Install
App" link unconditionally, then in the consuming component (e.g.,
SidebarWrapper.vue's <script setup>) compute a client-only boolean using
onMounted or a mounted reactive (or a computed inside ClientOnly) that checks
window.matchMedia('(display-mode: standalone)').matches and filter out the item
with nameKey 'nav.installApp' (or conditionally render it) based on that boolean
so the decision happens only after hydration.
In `@components/layout/navigation/SidebarOverlay.vue`:
- Around line 39-47: The leave transition for HeadlessTransitionChild currently
uses duration-0 causing an instant snap; update the leave props on
HeadlessTransitionChild (the component with
enter/enter-from/enter-to/leave/leave-from/leave-to) to use a non-zero
transition duration and easing (e.g., match enter: "transform transition
ease-in-out duration-300") so the panel animates out smoothly while keeping
leave-from="translate-x-0" and leave-to="translate-x-full".
In `@components/pages/home/FeaturedTags.vue`:
- Around line 95-108: Update the NuxtImg usage for featured tags: in the NuxtImg
component (see NuxtImg in FeaturedTags.vue and the isPriorityMedia logic that
decides preload), increase the quality prop from "60" to a higher value (e.g.,
75–85) and allow 2x density variants by removing or expanding densities (e.g.,
use "1x 2x" or omit densities) so featured images render sharper on high‑DPI
displays; keep the existing preload/fetchPriority logic intact.
In `@components/pages/posts/post/PostMedia.vue`:
- Around line 28-34: The current canUseImgproxy computed uses import.meta.dev
which forces imgproxy on during dev even for localhost; change its logic so
imgproxy is disabled for local preview hosts in dev by making canUseImgproxy
computed(() => !isLocalPreviewHost.value && !import.meta.dev) (use the existing
isLocalPreviewHost and requestUrl symbols) so imgproxy is only enabled when not
a local preview and not in dev.
In `@components/pages/posts/post/PostSource.vue`:
- Around line 78-92: Replace the try/catch new URL usage in parseSourceUrl,
isSourceAnUrl, and getHostnameFromUrl with the native URL.canParse() and
URL.parse() APIs: implement parseSourceUrl to call URL.canParse(source) and
return URL.parse(source) (or null if cannot parse), update isSourceAnUrl to use
URL.canParse(source) directly, and change getHostnameFromUrl to obtain the
hostname from the parsed result (if parseSourceUrl returned a URL object) or
fall back to the original source string; reference the existing functions
parseSourceUrl, isSourceAnUrl, and getHostnameFromUrl when making the changes.
In `@components/shared/ContentContainer.vue`:
- Around line 20-22: getLinkRel currently only takes href and drops rel for
internal links, which removes noopener for links opened in a new tab; modify
getLinkRel to accept the link target (e.g., add a second parameter target?:
string) and return at minimum "noopener noreferrer" whenever target ===
"_blank", and append "nofollow" when isExternalHref(href) is true. Update the
places that call getLinkRel (the link rendering near the current usage around
the existing link code at ~52-53) to pass the target attribute through so
internal _blank links keep "noopener noreferrer" while external _blank links get
"noopener noreferrer nofollow".
In `@composables/useLazyToast.ts`:
- Around line 13-14: The current code in useLazyToast sets toastPromise to the
dynamic import result and leaves it as-is on failure, which permanently caches a
failed import; update the toastPromise assignment inside useLazyToast to attach
a .catch handler that clears (resets) toastPromise (e.g., set back to
undefined/null) when the import fails and then rethrows or returns a fallback so
subsequent calls will retry the dynamic import; ensure you reference the
toastPromise variable and the useLazyToast function while making this change.
In `@Dockerfile`:
- Around line 14-15: Update the Dockerfile to avoid unconditional source-map
upload by making SENTRY_UPLOAD_SOURCE_MAPS a build argument or conditional on
presence of SENTRY_ORG, SENTRY_PROJECT, and SENTRY_AUTH_TOKEN; change the
current hardcoded SENTRY_UPLOAD_SOURCE_MAPS=true to derive its value from a new
ARG (e.g., ARG SENTRY_UPLOAD_SOURCE_MAPS=false) or compute it only when all
three build args (SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN) are provided,
and ensure the ENV/SENTRY_UPLOAD_SOURCE_MAPS line uses that ARG/conditional so
uploads only occur for production builds where credentials are set.
In `@pages/index.vue`:
- Around line 651-656: The breadcrumb schema uses a hardcoded '/' for the home
item in defineBreadcrumb, which breaks localized routes; update the breadcrumb
item's item value to use localePath('/') (e.g., item: localePath('/')) so the
structured data matches current locale-aware routes and ensure localePath is
available in the component where defineBreadcrumb is called.
In `@pages/other-sites.vue`:
- Around line 24-31: The breadcrumb structured-data currently uses
localePath(...) for item URLs inside useSchemaOrg([... defineBreadcrumb({...})
...]), which yields host-relative URLs; change the breadcrumb item values to
build absolute canonical URLs by prefixing localePath(...) with
project.urls.production (e.g., project.urls.production + localePath('/...')) so
defineBreadcrumb receives production absolute URLs for each item; update both
occurrences where localePath('/') and localePath('/other-sites') are used inside
defineBreadcrumb to use project.urls.production + localePath(...)
In `@pages/premium/backup.vue`:
- Around line 70-78: The breadcrumb schema currently uses localePath(...) to
build item URLs in the useSchemaOrg/defineBreadcrumb call; replace those
host-dependent URLs with absolute production URLs from project.urls.production
by concatenating the production base (project.urls.production) with the
locale-aware route path (e.g., the same path strings used in localePath calls
like '/', '/premium', '/premium/backup') so the item values become stable
canonical URLs; update the defineBreadcrumb itemListElement entries to use
project.urls.production + path (preserving any locale segments if needed)
instead of localePath(...).
In `@pages/premium/dashboard.vue`:
- Around line 129-137: The breadcrumb items are using relative localePath(...)
values which can yield host-dependent structured data; update the
useSchemaOrg/defineBreadcrumb call to build absolute canonical URLs using the
production origin (project.urls.production.origin or project.urls.production)
instead of raw localePath. Replace each localePath(...) passed into item with an
absolute URL created from the production origin plus the localePath result
(e.g., new URL(localePath('/...'), project.urls.production.origin) or
string-concatenate project.urls.production.origin + localePath(...)) so the
itemListElement entries are canonical, production-origin URLs.
In `@plugins/035.sentry.client.ts`:
- Around line 46-53: The code is dynamically importing private Sentry internals
(../node_modules/@sentry/nuxt/build/module/runtime/utils.js); remove that deep
import and replace usage by calling the supported public APIs from `@sentry/nuxt`
or `@sentry/browser` instead—e.g., import Sentry and the Nuxt helper exports
directly from '`@sentry/nuxt`' or use '`@sentry/browser`' public helpers, and
refactor any references to isNuxtError, reportNuxtError, and
buildSentryClientInitOptions to their public equivalents or reimplement the
small pieces you need (error detection/reporting and client init options) using
documented initialization patterns in the plugin where Sentry is initialized.
---
Outside diff comments:
In `@pages/posts/`[domain].vue:
- Around line 824-834: The client-side canonicalUrl computed currently only
retains the tags query (computed named canonicalUrl using getSingleQueryValue
and route.query) causing paginated and flat filter params (e.g. page and
filter[...] keys) to be lost during hydration; update the canonicalUrl
computation to include the existing page and any flat filter[...] params from
route.query (preserve project.urls.production.origin and route.path), encoding
them into the query string alongside tags, and then re-apply via useHead as
before; also ensure SSR patching remains via
server/plugins/fix-canonical-queries.ts and follow the guideline to re-apply
canonical after i18n hydration by using useSeoMeta in the page component so
canonical reflects page and filter[...] on both SSR and client hydration.
In `@pages/premium/saved-posts/`[domain].vue:
- Around line 90-98: The computed selectedTags currently assumes
route.query.tags is a string and calls .split('|'), which will throw if it's a
string[]; update selectedTags to first normalize tags: const raw =
route.query.tags as string | string[] | undefined; if (!raw) return []; const
tagList = Array.isArray(raw) ? raw : raw.split('|'); then map tagList with new
Tag({ name: tag }).toJSON(); this uses the selectedTags symbol and
route.query.tags and avoids calling .split on an array.
🪄 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: 656ac820-aebd-4e8e-bc3b-3dc483c616e6
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (69)
.vscode/settings.jsonAGENTS.mdDockerfileapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinkItems.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/SidebarOverlay.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.ts
💤 Files with no reviewable changes (4)
- server/plugins/lcp-preload-fetchpriority.ts
- app.vue
- package.json
- app/router.options.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
Dockerfile
📄 CodeRabbit inference engine (AGENTS.md)
Use multi-stage Docker builds with build stage including
SENTRY_*args and production stage copying only.output/directory (no node_modules needed)Select deployment target via
NITRO_PRESETbuild arg in Docker production build
Files:
Dockerfile
**/*.{js,ts,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use
URL.canParse()orURL.parse()for URL validation/parsing instead of constructortry/catch, preferringURL.parse()when the parsed URL object is needed
Files:
composables/navigation/useMenu.tscomposables/useIdleTask.tspages/premium/sign-in.vuecomponents/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vueassets/js/nuxt-image/imgproxy.provider.tspages/settings.vuepages/legal.vuepages/dmca.vuetest/pages/premium/backup.test.tstest/assets/router-helper.test.tscomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/modal/ReviewPrompt.vuepages/privacy-policy.vuepages/cookie-policy.vuecomponents/pages/home/FeaturedTags.vuepages/terms-of-service.vuecomponents/pages/home/PageHistory.vueplugins/040.matomo.client.tscomponents/layout/navigation/SidebarOverlay.vuecomposables/useInteractionDetector.tscomposables/useLazyToast.tspages/premium/forgot-password.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostTag.vuetest/server-mocks/plugin.tscomponents/pages/posts/PostsPageFooter.vuecomponents/shared/ShareButton.vuepages/other-sites.vueassets/js/sidebarLinks.tsassets/js/sidebarLinkItems.tscomponents/pages/home/PageHistorySection.vuepages/premium/backup.vueplugins/035.sentry.client.tscomponents/layout/navigation/SidebarWrapper.vuecomposables/useFaviconUrl.tslayouts/default.vuepages/premium/dashboard.vuecomponents/layout/navigation/Navbar.vuepages/premium/saved-posts/[domain].vuetest/server/fix-canonical-queries.test.tsserver/api/_sitemap-urls.tscomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/tag-collections.vueassets/js/RouterHelper.tscomponents/pages/posts/navigation/DomainSelector.vuecomponents/shared/ContentContainer.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/index.vuecomponents/pages/posts/post/PostSource.vuepages/tags/[domain]/[tag].vuepages/premium/additional-boorus.vuetest/pages/posts.test.tsnuxt.config.jscomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
**/*.{js,ts,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/navigation/useMenu.tscomposables/useIdleTask.tspages/premium/sign-in.vuecomponents/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vueassets/js/nuxt-image/imgproxy.provider.tspages/settings.vuepages/legal.vuepages/dmca.vuetest/pages/premium/backup.test.tstest/assets/router-helper.test.tsi18n/locales/ja.jsoncomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/modal/ReviewPrompt.vuei18n/locales/es.jsoni18n/locales/ru.jsonpages/privacy-policy.vuepages/cookie-policy.vuecomponents/pages/home/FeaturedTags.vuepages/terms-of-service.vuecomponents/pages/home/PageHistory.vueplugins/040.matomo.client.tscomponents/layout/navigation/SidebarOverlay.vuecomposables/useInteractionDetector.tscomposables/useLazyToast.tspages/premium/forgot-password.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostTag.vuetest/server-mocks/plugin.tscomponents/pages/posts/PostsPageFooter.vuecomponents/shared/ShareButton.vuepages/other-sites.vueassets/js/sidebarLinks.tsassets/js/sidebarLinkItems.tscomponents/pages/home/PageHistorySection.vuepages/premium/backup.vuei18n/locales/en.jsonplugins/035.sentry.client.tscomponents/layout/navigation/SidebarWrapper.vuecomposables/useFaviconUrl.tslayouts/default.vuepages/premium/dashboard.vuecomponents/layout/navigation/Navbar.vuepages/premium/saved-posts/[domain].vuetest/server/fix-canonical-queries.test.tsserver/api/_sitemap-urls.tscomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/tag-collections.vueassets/js/RouterHelper.tscomponents/pages/posts/navigation/DomainSelector.vuecomponents/shared/ContentContainer.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/index.vuecomponents/pages/posts/post/PostSource.vuepages/tags/[domain]/[tag].vuepages/premium/additional-boorus.vuetest/pages/posts.test.tsnuxt.config.jscomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use
useSeoMetain page components for page-specific tags (title, description)Use
server/plugins/fix-canonical-queries.tsto patch canonical links in SSR HTML; useuseHeadwithuseSeoMetain page components to re-apply canonical after i18n hydration
Files:
pages/premium/sign-in.vuepages/settings.vuepages/legal.vuepages/dmca.vuepages/privacy-policy.vuepages/cookie-policy.vuepages/terms-of-service.vuepages/premium/forgot-password.vuepages/other-sites.vuepages/premium/backup.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuepages/premium/tag-collections.vuepages/index.vuepages/premium/index.vuepages/tags/[domain]/[tag].vuepages/premium/additional-boorus.vuepages/posts/[domain].vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/imagev2 module API for image preload priority viapreload: { fetchPriority: 'high' }instead of patching rendered HTML in Nitro
Files:
pages/premium/sign-in.vuecomponents/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vuepages/settings.vuepages/legal.vuepages/dmca.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/modal/ReviewPrompt.vuepages/privacy-policy.vuepages/cookie-policy.vuecomponents/pages/home/FeaturedTags.vuepages/terms-of-service.vuecomponents/pages/home/PageHistory.vuecomponents/layout/navigation/SidebarOverlay.vuepages/premium/forgot-password.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/shared/ShareButton.vuepages/other-sites.vuecomponents/pages/home/PageHistorySection.vuepages/premium/backup.vuecomponents/layout/navigation/SidebarWrapper.vuelayouts/default.vuepages/premium/dashboard.vuecomponents/layout/navigation/Navbar.vuepages/premium/saved-posts/[domain].vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/shared/ContentContainer.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/index.vuecomponents/pages/posts/post/PostSource.vuepages/tags/[domain]/[tag].vuepages/premium/additional-boorus.vuecomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Import Vue components without path prefix (e.g.,
<DomainSelector>not<Input/DomainSelector>) due topathPrefix: falseconfigDisable imgproxy on localhost/127.0.0.1 previews in PostMedia to allow local development; validate image delivery with production-host HTML
Files:
components/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/PageHistory.vuecomponents/layout/navigation/SidebarOverlay.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/shared/ShareButton.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vue
test/**/*.{test,spec}.{js,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest +
@nuxt/test-utilswith Playwright browser mode for testing; callawait setup({ browser: true })within describe blocksImport
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo during test debugging
Files:
test/pages/premium/backup.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
plugins/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep
@formkit/auto-animateroute-scoped rather than global to reduce entry bundle size by ~3 KB gzipOrganize plugins in numeric order (
020.,030.,035.,040.,050.) for controlled load sequence
Files:
plugins/040.matomo.client.tsplugins/050.formbricks.client.tsplugins/035.sentry.client.ts
nuxt.config.{js,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Keep
features.inlineStyles: falsein Nuxt config to avoid doubling HTML size; enabling inline styles increased homepage HTML from ~51 KB to ~106 KBMirror route rules in
nuxt.configfor prefixed i18n paths using themirroredRouteRules()helper to maintain cache/SSR consistencyPlace static global head tags (favicon, rating, monetization, color-scheme) in
nuxt.config.jshead.metaSet
$test.runtimeConfig.public.apiUrlto empty string in test mode so$fetch(baseURL: '')routes to local Nitro test serverDisable Sentry in tests via
$test.sentry.enabled: falsein nuxt.config.js
Files:
nuxt.config.js
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Use `project.urls.production` for canonical URLs to prevent duplicate content across clone domains
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Do not re-add `qs` library for nested `route.query.filter` objects; keep flat bracket keys (`filter[sort]`, `filter[rating]`) with Vue Router's default query handling
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; verify performance changes with production builds, tests, and Lighthouse before keeping changes
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Keep global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Ensure Node ≥ 24 is installed as specified in `package.json` engines
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Clone the repository with `--recursive` to include the Git submodule at `assets/lib/rule-34-shared-resources`
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Configure the external API URL via `NUXT_PUBLIC_API_URL` environment variable (default `http://localhost:8081`) that points to the API service
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T20:48:53.669Z
Learning: Upload source maps to Sentry only in production Docker builds when `SENTRY_ORG`, `SENTRY_PROJECT`, and `SENTRY_AUTH_TOKEN` build args are provided
🔇 Additional comments (49)
.vscode/settings.json (1)
2-2: LGTM!composables/navigation/useMenu.ts (1)
1-1: LGTM!composables/useIdleTask.ts (1)
1-16: LGTM!pages/premium/sign-in.vue (1)
8-8: LGTM!Also applies to: 61-69
components/pages/home/Newsletter.vue (1)
45-45: LGTM!components/layout/ClientToaster.vue (1)
1-13: LGTM!assets/js/nuxt-image/imgproxy.provider.ts (1)
41-41: LGTM!Also applies to: 83-83
pages/settings.vue (1)
32-39: LGTM!pages/legal.vue (1)
12-20: LGTM!pages/dmca.vue (1)
12-20: LGTM!test/pages/premium/backup.test.ts (2)
25-46: LGTM!
110-110: ⚡ Quick winNo issue found. The mock data file
backup.mock-data.jsonexists attest/pages/premium/backup.mock-data.json, as expected by the test.test/assets/router-helper.test.ts (1)
24-41: LGTM!Also applies to: 43-61, 63-66
i18n/locales/ja.json (1)
402-402: LGTM!components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-30
components/layout/modal/ReviewPrompt.vue (1)
47-50: LGTM!i18n/locales/es.json (1)
402-402: LGTM!i18n/locales/ru.json (1)
402-402: LGTM!pages/privacy-policy.vue (1)
5-5: LGTM!Also applies to: 11-20
pages/cookie-policy.vue (1)
5-5: LGTM!Also applies to: 12-20
components/pages/home/FeaturedTags.vue (3)
4-22: LGTM!
29-38: LGTM!
62-62: LGTM!pages/terms-of-service.vue (1)
5-5: LGTM!Also applies to: 12-20
components/pages/home/PageHistory.vue (1)
32-72: LGTM!As per coding guidelines: the refactor correctly uses
URL.parse()preferentially with a fallback toURL.canParse()for URL validation/parsing instead of the constructortry/catchpattern.public/llms.txt (1)
1-27: ⚡ Quick winNo action required — the URLs in
llms.txtcorrectly match the configured canonical URL inproject.urls.production(https://r34.app/). Hardcoded URLs in static public files likellms.txtandrobots.txtare standard practice and require no changes.plugins/040.matomo.client.ts (1)
2-2: LGTM!Also applies to: 41-42, 47-47
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
pages/premium/forgot-password.vue (1)
5-5: LGTM!Also applies to: 40-48
plugins/050.formbricks.client.ts (1)
2-2: LGTM!Also applies to: 25-26, 31-33
components/pages/posts/post/PostTag.vue (1)
2-15: LGTM!Also applies to: 31-31
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-138
components/pages/posts/PostsPageFooter.vue (1)
2-6: LGTM!Also applies to: 8-8, 125-125
components/shared/ShareButton.vue (1)
11-13: LGTM!assets/js/sidebarLinks.ts (1)
10-25: LGTM!components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!i18n/locales/en.json (1)
402-402: LGTM!server/api/_sitemap-urls.ts (1)
9-13: LGTM!Also applies to: 19-19, 24-24
AGENTS.md (1)
25-25: LGTM!Also applies to: 38-38, 67-67, 83-85, 94-123
components/pages/posts/navigation/search/TagCollections.vue (1)
17-17: LGTM!Also applies to: 86-90, 114-115, 118-121, 136-139
pages/premium/tag-collections.vue (1)
3-3: LGTM!Also applies to: 10-12, 147-156, 185-189, 197-200, 219-223, 237-241, 253-257, 266-270, 346-350
assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-46, 62-70
components/pages/posts/navigation/DomainSelector.vue (1)
23-23: LGTM!Also applies to: 73-73, 90-93, 125-125, 145-148
components/pages/posts/post/PostComponent.vue (1)
15-32: LGTM!Also applies to: 68-74, 90-90, 174-174, 179-270
pages/premium/index.vue (1)
22-27: LGTM!Also applies to: 65-77, 88-101, 112-124, 135-148, 184-189, 209-214, 673-676
pages/tags/[domain]/[tag].vue (1)
1-321: LGTM!pages/premium/additional-boorus.vue (1)
11-17: LGTM!Also applies to: 185-193, 210-213, 217-219, 238-241, 247-250, 270-273, 289-292, 305-308, 318-322, 425-429
test/pages/posts.test.ts (1)
3-3: LGTM!Also applies to: 26-26, 61-61, 74-74, 96-110, 144-157, 177-177, 206-220, 235-248, 279-279, 294-295, 304-313, 331-343, 359-372, 374-452
nuxt.config.js (1)
9-42: LGTM!Also applies to: 90-90, 98-98, 106-106, 139-180, 185-191, 195-198, 212-212, 285-285, 403-403, 465-465
| // Hide "Install App" when in standalone mode | ||
| ...(import.meta.client && window.matchMedia('(display-mode: standalone)').matches | ||
| ? [] | ||
| : [ | ||
| { | ||
| nameKey: 'nav.installApp', | ||
| href: `https://www.installpwa.com/from/${project.urls.production.hostname}`, | ||
| isExternal: true | ||
| } | ||
| ]), |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Check module-scope standalone condition:"
rg -n -C2 "import.meta.client && window.matchMedia\\('\\(display-mode: standalone\\)'\\)\\.matches" assets/js/sidebarLinkItems.ts
echo
echo "Find sidebar navigation consumers:"
rg -n -C3 "sidebarNavigation|sidebarLinkItems" assets/js components layouts pages
echo
echo "Check if consuming nav is rendered without ClientOnly wrappers:"
rg -n -C3 "ClientOnly|Sidebar|sidebarNavigation" components/layout layoutsRepository: Rule-34/App
Length of output: 8512
Move the standalone mode check to component level to prevent SSR/client list divergence.
This module-scope condition evaluates differently on server and client: the server always includes "Install App" (since import.meta.client is false), while the client may exclude it if in standalone mode. Even though data-allow-mismatch suppresses hydration warnings in SidebarWrapper.vue, the underlying functional mismatch persists—links can change after hydration, causing unexpected behavior.
Refactor by moving the condition into the component's <script setup> block as a computed property or conditional rendering within ClientOnly, or handle standalone mode detection after hydration.
🤖 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 `@assets/js/sidebarLinkItems.ts` around lines 15 - 24, The module-level
standalone detection in assets/js/sidebarLinkItems.ts causes SSR/client
mismatch; remove the import.meta.client && window.matchMedia('(display-mode:
standalone)').matches conditional from the exported link list and instead export
the full "Install App" link unconditionally, then in the consuming component
(e.g., SidebarWrapper.vue's <script setup>) compute a client-only boolean using
onMounted or a mounted reactive (or a computed inside ClientOnly) that checks
window.matchMedia('(display-mode: standalone)').matches and filter out the item
with nameKey 'nav.installApp' (or conditionally render it) based on that boolean
so the decision happens only after hydration.
| <NuxtImg | ||
| :alt="t('common.featuredTag', { name: tag.name })" | ||
| :decoding="index <= 4 ? undefined : 'async'" | ||
| :loading="index <= 4 ? undefined : 'lazy'" | ||
| :preload="index <= 4" | ||
| :decoding="isPriorityMedia(index) ? undefined : 'async'" | ||
| :fetchpriority="isPriorityMedia(index) ? undefined : 'low'" | ||
| :loading="isPriorityMedia(index) ? 'eager' : 'lazy'" | ||
| :preload="isPriorityMedia(index) ? { fetchPriority: 'high' } : false" | ||
| :src="tag.media[0].src" | ||
| class="h-auto w-full rounded-t-md" | ||
| height="600" | ||
| width="400" | ||
| densities="1x" | ||
| format="webp" | ||
| height="288" | ||
| quality="60" | ||
| width="192" | ||
| /> |
There was a problem hiding this comment.
Verify image quality and density settings for featured content.
Two configuration choices may degrade visual quality on featured tags:
quality="60"is quite low and may introduce visible compression artifacts, especially for featured/hero content where higher quality is typically expected.densities="1x"prevents generation of 2× variants, so images may appear pixelated on retina/high-DPI displays (common on modern mobile devices and desktops).
Consider:
- Increasing quality to 75-85 for featured images
- Using
densities="1x 2x"or removing the densities prop to allow automatic density selection
The preload: { fetchPriority: 'high' } implementation correctly follows the coding guideline for @nuxt/image v2 API.
🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 95 - 108, Update the
NuxtImg usage for featured tags: in the NuxtImg component (see NuxtImg in
FeaturedTags.vue and the isPriorityMedia logic that decides preload), increase
the quality prop from "60" to a higher value (e.g., 75–85) and allow 2x density
variants by removing or expanding densities (e.g., use "1x 2x" or omit
densities) so featured images render sharper on high‑DPI displays; keep the
existing preload/fetchPriority logic intact.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.otherSites.title'), item: localePath('/other-sites') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Use production absolute URLs for breadcrumb items.
The breadcrumb item values are built from locale-relative paths, so structured-data URLs vary by serving host. Build them from project.urls.production plus localePath(...) to keep a single canonical domain.
Suggested fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+ const otherSitesBreadcrumbUrl = new URL(localePath('/other-sites'), project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.otherSites.title'), item: localePath('/other-sites') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.otherSites.title'), item: otherSitesBreadcrumbUrl }
]
})
])📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.otherSites.title'), item: localePath('/other-sites') } | |
| ] | |
| }) | |
| ]) | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const otherSitesBreadcrumbUrl = new URL(localePath('/other-sites'), project.urls.production).toString() | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.otherSites.title'), item: otherSitesBreadcrumbUrl } | |
| ] | |
| }) | |
| ]) |
🤖 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 `@pages/other-sites.vue` around lines 24 - 31, The breadcrumb structured-data
currently uses localePath(...) for item URLs inside useSchemaOrg([...
defineBreadcrumb({...}) ...]), which yields host-relative URLs; change the
breadcrumb item values to build absolute canonical URLs by prefixing
localePath(...) with project.urls.production (e.g., project.urls.production +
localePath('/...')) so defineBreadcrumb receives production absolute URLs for
each item; update both occurrences where localePath('/') and
localePath('/other-sites') are used inside defineBreadcrumb to use
project.urls.production + localePath(...)
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.dashboard.seoTitle'), item: localePath('/premium/dashboard') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Use production-origin URLs for breadcrumb item values.
Relative localePath(...) items can produce host-dependent structured data on clone domains. Use absolute URLs from project.urls.production.origin for breadcrumb entries to keep schema canonical.
Based on learnings: Use project.urls.production for canonical URLs to prevent duplicate content across clone domains.
🤖 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 `@pages/premium/dashboard.vue` around lines 129 - 137, The breadcrumb items are
using relative localePath(...) values which can yield host-dependent structured
data; update the useSchemaOrg/defineBreadcrumb call to build absolute canonical
URLs using the production origin (project.urls.production.origin or
project.urls.production) instead of raw localePath. Replace each localePath(...)
passed into item with an absolute URL created from the production origin plus
the localePath result (e.g., new URL(localePath('/...'),
project.urls.production.origin) or string-concatenate
project.urls.production.origin + localePath(...)) so the itemListElement entries
are canonical, production-origin URLs.
416be9d to
db17695
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
test/pages/posts.test.ts (1)
323-343:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAssert the canonical origin, not just the path.
These checks still pass if the app emits a clone-domain canonical, so they won't catch the duplicate-content regression this SEO coverage is supposed to guard against. Assert the full production URL in these cases instead of only checking
pathname/toContain(...).As per coding guidelines "For canonical URLs, always point to production (
https://r34.app/…) even when served from clone domains, usingproject.urls.production— this is intentional for SEO to prevent duplicate content"Also applies to: 345-372, 422-451
🤖 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/pages/posts.test.ts` around lines 323 - 343, The test currently only asserts the canonical link's path (using page.locator('link[rel="canonical"]') and expect(...).toContain('/posts/safebooru.org?tags=1girl')), which allows clone-domain canonical URLs to slip by; update the assertion to verify the full production canonical URL by comparing against project.urls.production combined with the expected path (e.g., ensure canonicalHref startsWith or equals `${project.urls.production}/posts/safebooru.org?tags=1girl`) in the 'preserves tags in canonical link after client-side hydration' test (and the similar assertions at the other ranges), so the test enforces the production origin for canonical URLs rather than only the pathname.
♻️ Duplicate comments (1)
plugins/035.sentry.client.ts (1)
46-52:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove the deep
@sentry/nuxtimport.
../node_modules/@sentry/nuxt/build/module/runtime/utils.jsis a private implementation path, so this init flow now depends on Sentry's internal file layout and package exports staying unchanged. If that import breaks, the deferred client init path never completes and browser errors stop being reported. Reimplement the small helper behavior locally or switch to documented public APIs instead.🤖 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 `@plugins/035.sentry.client.ts` around lines 46 - 52, The code is importing a private runtime helper via a deep path which is fragile; remove the deep import ('../node_modules/@sentry/nuxt/build/module/runtime/utils.js') from the Promise.all and instead either (a) implement the small helper logic locally (replicate the behavior of isNuxtError, reportNuxtError and buildSentryClientInitOptions used here) or (b) switch to documented public APIs from `@sentry/nuxt` that provide equivalent functionality; update the references to isNuxtError, reportNuxtError and buildSentryClientInitOptions to point to your local helper module or the public API so the deferred client init flow no longer depends on Sentry's internal file layout.
🤖 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/home/FeaturedTags.vue`:
- Around line 29-33: selectedTags leaves tag.media as slice(0,1) which yields []
for empty media and the template reads tag.media[0] unguarded; update the
computed selectedTags to guarantee an element at index 0 (or a safe default) so
indexing won’t throw: in the selectedTags computed (which maps props.tags) set
media to tag.media.length ? tag.media.slice(0,1) : [null] or add a mediaFirst:
tag.media[0] ?? null property and update template references accordingly
(referencing selectedTags, props.tags, tag.media, and tag.media[0]).
In `@components/pages/posts/navigation/search/TagCollections.vue`:
- Line 17: The call to useLazyToast() returns a toast whose methods (e.g.,
toast.error) are async and must be awaited; update all call sites in
TagCollections.vue that call toast.error (and any other toast.* methods) to
await the call and propagate async by marking the enclosing handler or method
async (for example, make the click/submit handler or the component method async)
and handle errors with try/catch if appropriate so the lazy import failure does
not produce unhandled promise rejections.
In `@composables/useIdleTask.ts`:
- Line 12: The fallback setTimeout call in useIdleTask ignores the timeout
parameter and uses a hardcoded 1ms; update the fallback to use the timeout
parameter (the timeout argument of useIdleTask) so the non-requestIdleCallback
path respects the same timeout semantics as the requestIdleCallback branch—i.e.,
replace the setTimeout invocation in composables/useIdleTask.ts to schedule with
timeout instead of 1ms.
In `@nuxt.config.js`:
- Around line 138-164: The listed static icon routes ('/icon.svg',
'/favicon.ico', '/apple-touch-icon-180x180.png', '/pwa-64x64.png',
'/pwa-192x192.png', '/pwa-512x512.png', '/maskable-icon-512x512.png') are being
served with immutableCacheHeaders which prevents revalidation; change those
route entries in nuxt.config.js to use a non-immutable cache policy (e.g., a
normal max-age/cache-control header or a predefined cacheHeaders variable) or
alternatively stop serving fixed filenames and use versioned filenames, ensuring
you update the entries that reference immutableCacheHeaders to the new header
config so icons can be updated after deploy.
- Around line 194-198: Remove the Nitro precompression configuration: delete the
compressPublicAssets object under the nitro config (the compressPublicAssets
block that sets gzip: true and brotli: true) so the Nitro build stops generating
.gz/.br artifacts; leave the rest of the nitro config intact and do not add any
alternative app-level compression settings.
In `@pages/cookie-policy.vue`:
- Around line 12-20: The breadcrumb items currently use localePath(...) which
yields host-relative URLs; update the useSchemaOrg -> defineBreadcrumb call so
itemListElement[*].item builds canonical production absolute URLs using
project.urls.production (e.g., replace localePath('/') and
localePath('/cookie-policy') with project.urls.production + '/' and
project.urls.production + '/cookie-policy', ensuring proper slashes), leaving
t(...) and the rest of defineBreadcrumb/defineWebPage unchanged.
In `@pages/premium/forgot-password.vue`:
- Around line 40-48: The breadcrumb schema is using host-relative URLs via
localePath(...) inside useSchemaOrg/defineBreadcrumb; change the breadcrumb
`item` values to production-absolute URLs built from project.urls.production as
the base plus the localized path (e.g., combine project.urls.production with the
result of localePath('/...') or use new URL(localePath(...),
project.urls.production). Update the three item entries so they use the
production base instead of raw localePath results while keeping the same display
names.
In `@pages/tags/`[domain]/[tag].vue:
- Around line 223-236: The two hardcoded CTA labels in
pages/tags/[domain]/[tag].vue should be replaced with i18n translations: change
the "Browse all {{ formattedTag }} posts" and "More from {{ selectedBooru.domain
}}" literals to use the translator (e.g. $t or useI18n()) with descriptive keys
like "tags.browse_all_posts" and "tags.more_from_booru", passing formattedTag
and selectedBooru.domain as interpolation params (use fullPostsRoute,
formattedTag, and selectedBooru.domain to locate the nodes to update); ensure
you add corresponding keys to your locale files (ru/es/ja) with placeholders for
the tag/domain.
- Around line 282-290: Consolidate the separate preload boolean and
fetchpriority imgAttr into a single preload object: replace the current use of
:preload="isPriorityPost(index)" and the fetchpriority entry inside :imgAttrs
with a single :preload prop that returns isPriorityPost(index) ? {
fetchPriority: 'high' } : false; keep getPostAspectRatio(post) and the rest of
imgAttrs (class, style) intact and remove fetchpriority from imgAttrs so the
component follows the `@nuxt/image` v2 API and matches the pattern used in
components/pages/home/FeaturedTags.vue.
- Around line 30-32: fullPostsRoute currently hand-builds a ?tags= query string
(computed fullPostsRoute) instead of using Vue Router serialization; replace its
implementation to return the route object produced by
generatePostsRoute(selectedBooru.value.domain, { tags: tagParam.value }) (or the
correct generatePostsRoute signature) so Vue Router handles query encoding and
flat bracket notation, and also wrap the CTA label strings "Browse all" and
"More from" with the t() composable (e.g., t('Browse all')) where they are used
so they are localized like the rest of the page.
In `@test/pages/posts.test.ts`:
- Around line 411-420: The test "renders tag landing page with posts" currently
uses the generic page.locator('img').first() which can match browser chrome;
change it to wait for and assert a post-card-specific element instead: replace
the generic locator with one that targets your post card (for example,
page.locator('.post-card img').first() or
page.getByTestId('post-card').locator('img').first()), keeping the rest of the
flow (createTrackedPage, page.goto, heading checks) intact so the test verifies
the actual post grid rendered.
---
Outside diff comments:
In `@test/pages/posts.test.ts`:
- Around line 323-343: The test currently only asserts the canonical link's path
(using page.locator('link[rel="canonical"]') and
expect(...).toContain('/posts/safebooru.org?tags=1girl')), which allows
clone-domain canonical URLs to slip by; update the assertion to verify the full
production canonical URL by comparing against project.urls.production combined
with the expected path (e.g., ensure canonicalHref startsWith or equals
`${project.urls.production}/posts/safebooru.org?tags=1girl`) in the 'preserves
tags in canonical link after client-side hydration' test (and the similar
assertions at the other ranges), so the test enforces the production origin for
canonical URLs rather than only the pathname.
---
Duplicate comments:
In `@plugins/035.sentry.client.ts`:
- Around line 46-52: The code is importing a private runtime helper via a deep
path which is fragile; remove the deep import
('../node_modules/@sentry/nuxt/build/module/runtime/utils.js') from the
Promise.all and instead either (a) implement the small helper logic locally
(replicate the behavior of isNuxtError, reportNuxtError and
buildSentryClientInitOptions used here) or (b) switch to documented public APIs
from `@sentry/nuxt` that provide equivalent functionality; update the references
to isNuxtError, reportNuxtError and buildSentryClientInitOptions to point to
your local helper module or the public API so the deferred client init flow no
longer depends on Sentry's internal file layout.
🪄 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: 7a30d15b-46c9-4084-a719-b6eccb4cea20
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (71)
.vscode/settings.jsonAGENTS.mdDockerfileapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarOverlay.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.ts
💤 Files with no reviewable changes (5)
- package.json
- server/plugins/lcp-preload-fetchpriority.ts
- app.vue
- app/router.options.ts
- components/layout/navigation/SidebarWrapper.vue
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Component auto-imports use no path prefix — import components as
<DomainSelector>not<Input/DomainSelector>(due topathPrefix: falsein nuxt.config)Use
useSeoMetain page components for page-specific SEO tags (title, description)For canonical URLs, always point to production (
https://r34.app/…) even when served from clone domains, usingproject.urls.production— this is intentional for SEO to prevent duplicate contentKeep
@formkit/auto-animateroute-scoped unless it is used broadly — localvAutoAnimateimports on premium CSR pages saved about 3 KB gzip vs. a global directiveDo not add
provideHeadlessUseIdinapp.vuewhile the project uses Vue 3.5+ and@headlessui/vue1.7.23+ — those versions use Vue's nativeuseIdand the workaround is only for older versionsUse key-based query filters with flat bracket notation (
filter[sort],filter[rating], etc.) and Vue Router's default query handling — do not re-addqsfor nestedroute.query.filterobjects unless the URL contract changes, asqsadds measurable parser/stringifier cost on first-loadUse
@nuxt/imagev2preload: { fetchPriority: 'high' }via the module API for image preload priority instead of patching rendered HTML in NitroDo not add service worker logic — the PWA service worker is intentionally disabled (
selfDestroying: true)In Vue templates, use single attribute per line with Prettier formatting
Files:
components/pages/home/Newsletter.vuecomponents/pages/home/PageHistorySection.vuepages/legal.vuepages/dmca.vuepages/privacy-policy.vuepages/premium/sign-in.vuecomponents/pages/posts/post/PostTag.vuecomponents/layout/ClientToaster.vuecomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/PostsPageFooter.vuepages/cookie-policy.vuepages/premium/backup.vuecomponents/pages/home/PageHistory.vuecomponents/shared/ContentContainer.vuepages/premium/forgot-password.vuecomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/home/FeaturedTags.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/layout/DialogManager.vuepages/premium/dashboard.vuelayouts/default.vuecomponents/layout/navigation/SidebarOverlay.vuepages/index.vuepages/premium/tag-collections.vuecomponents/pages/posts/post/PostSource.vuepages/settings.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/premium/index.vuepages/posts/[domain].vuepages/tags/[domain]/[tag].vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/navigation/DomainSelector.vue
**/*.{js,ts,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer high-impact, measurable optimizations over small rewrites — keep battle-tested dependencies unless replacing one has a clear, measured payoff
After substantial performance changes, verify with a production build, relevant tests, request traces, and Lighthouse against the built app before deciding the change is worth keeping
In test mode,
$test.runtimeConfig.public.apiUrlis set to''so$fetch(baseURL: '')routes to the local Nitro test serverSentry is fully disabled in tests via
$test.sentry.enabled: falseinnuxt.config.jsImport
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo in debug modeUse single quotes, no semicolons, trailing commas removed, 120-char print width as per Prettier configuration
Files:
components/pages/home/Newsletter.vuecomponents/pages/home/PageHistorySection.vuepages/legal.vuepages/dmca.vuecomposables/useInteractionDetector.tspages/privacy-policy.vuepages/premium/sign-in.vueplugins/040.matomo.client.tscomponents/pages/posts/post/PostTag.vuecomponents/layout/ClientToaster.vueplugins/050.formbricks.client.tscomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vuetest/pages/premium/backup.test.tscomposables/useFaviconUrl.tscomposables/navigation/useMenu.tscomponents/shared/ShareButton.vuecomponents/pages/posts/PostsPageFooter.vuepages/cookie-policy.vuepages/premium/backup.vuecomponents/pages/home/PageHistory.vuecomponents/shared/ContentContainer.vuepages/premium/forgot-password.vuecomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/home/FeaturedTags.vueassets/js/nuxt-image/imgproxy.provider.tscomposables/useLazyToast.tscomposables/useDialogManagerState.tscomponents/layout/navigation/Navbar.vueserver/api/_sitemap-urls.tscomponents/pages/posts/navigation/search/TagCollections.vuecomponents/layout/DialogManager.vuepages/premium/dashboard.vuelayouts/default.vuecomponents/layout/navigation/SidebarOverlay.vuecomposables/useIdleTask.tstest/assets/router-helper.test.tspages/index.vuepages/premium/tag-collections.vuetest/server-mocks/plugin.tscomponents/pages/posts/post/PostSource.vuepages/settings.vueassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tscomponents/pages/posts/post/PostComponent.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vueassets/js/sidebarLinks.tspages/premium/index.vuepages/posts/[domain].vueplugins/035.sentry.client.tspages/tags/[domain]/[tag].vuenuxt.config.jscomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/navigation/DomainSelector.vuetest/pages/posts.test.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
In
pages/posts/[domain].vue, useuseHeadto re-apply the canonical URL after i18n overwrites it during CSR hydration as a workaround for thecanonicalQueriesno-op bug in i18n v10
Files:
pages/legal.vuepages/dmca.vuepages/privacy-policy.vuepages/premium/sign-in.vuepages/terms-of-service.vuepages/cookie-policy.vuepages/premium/backup.vuepages/premium/forgot-password.vuepages/other-sites.vuepages/premium/dashboard.vuepages/index.vuepages/premium/tag-collections.vuepages/settings.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/premium/index.vuepages/posts/[domain].vuepages/tags/[domain]/[tag].vue
**/*.{js,ts,mjs,cjs}
📄 CodeRabbit inference engine (AGENTS.md)
Use camelCase for variable and function names (Prettier formatting standard)
Files:
composables/useInteractionDetector.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tstest/pages/premium/backup.test.tscomposables/useFaviconUrl.tscomposables/navigation/useMenu.tsassets/js/nuxt-image/imgproxy.provider.tscomposables/useLazyToast.tscomposables/useDialogManagerState.tsserver/api/_sitemap-urls.tscomposables/useIdleTask.tstest/assets/router-helper.test.tstest/server-mocks/plugin.tsassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tsassets/js/sidebarLinks.tsplugins/035.sentry.client.tsnuxt.config.jstest/pages/posts.test.ts
**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation and parsing, prefer
URL.canParse()orURL.parse()over constructortry/catch— useURL.parse()when the parsed URL object is needed, withURL.canParse()fallback in browser code if compatibility matters
Files:
composables/useInteractionDetector.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tstest/pages/premium/backup.test.tscomposables/useFaviconUrl.tscomposables/navigation/useMenu.tsassets/js/nuxt-image/imgproxy.provider.tscomposables/useLazyToast.tscomposables/useDialogManagerState.tsserver/api/_sitemap-urls.tscomposables/useIdleTask.tstest/assets/router-helper.test.tstest/server-mocks/plugin.tsassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tsassets/js/sidebarLinks.tsplugins/035.sentry.client.tstest/pages/posts.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright insidedescribeblocks that callawait setup({ browser: true })for integration tests
Files:
test/pages/premium/backup.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
test/server-mocks/plugin.ts
📄 CodeRabbit inference engine (AGENTS.md)
Mock server-side API calls via a test-only Nitro plugin at
test/server-mocks/plugin.ts, injected throughnuxt.config.js→$test.nitro.plugins
Files:
test/server-mocks/plugin.ts
nuxt.config.js
📄 CodeRabbit inference engine (AGENTS.md)
Place static global SEO tags (favicon, rating, monetization, color-scheme) in
nuxt.config.jshead.metaMirror route rules via the
mirroredRouteRules()helper so non-default locale prefixed paths (ru, es, ja) get the same caching and SSR rules as the default localeKeep
features.inlineStyles: false— enabling it doubled homepage HTML from 51 KB to 106 KB and increased Lighthouse byte weight from 361 KiB to 406 KiB without improving performance scoreKeep the global TanStack Vue Query plugin unless a larger measured payoff appears — a route-scoped experiment saved only 8 KB compressed on homepage and did not improve Lighthouse
Configure custom scroll behavior to skip scroll-to-top when only the
pagequery param changes between same-route navigationsConfigure a custom
imgproxyprovider for<NuxtImg>and deliberately generate images at 1x density only (webp format) to reduce bandwidthDo not add app-level HTML compression — production is behind Cloudflare which Brotli-compresses HTML responses; only add compression if a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Files:
nuxt.config.js
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Node ≥ 24 is required as specified in `package.json` engines
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Git submodule at `assets/lib/rule-34-shared-resources` must be cloned with `--recursive` flag during setup
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
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](https://github.com/Rule-34/API)
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: ESLint config exists but is not wired to any npm script — use Prettier for code formatting instead
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Use Nuxt 4 with Vue 3 and TypeScript for the application stack
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Use TailwindCSS v4 via `tailwindcss/vite` Vite plugin for styling
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Use Vitest with `nuxt/test-utils` and Playwright browser mode for testing
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Organize the single Nuxt app with key directories: `config/`, `app/`, `composables/`, `plugins/`, `server/api/`, `server/middleware/`, `server/plugins/`, `assets/js/`, `assets/lib/`, `i18n/locales/`, `components/`, `test/`
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: In Docker production builds, use a multi-stage setup where the build stage needs `SENTRY_*` args for source map uploads, and the production stage copies only `.output/` without `node_modules` (Nitro bundles everything)
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Select deployment target via `NITRO_PRESET` build arg in Docker production builds
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Source map uploads to Sentry only happen in production Docker builds (requires `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` build args)
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-17T22:05:35.435Z
Learning: Create `.env` file from `.example.env` template and edit configuration before running `npm install`
🔇 Additional comments (53)
components/pages/home/Newsletter.vue (1)
45-45: LGTM!i18n/locales/ja.json (1)
402-402: LGTM!i18n/locales/es.json (1)
402-402: LGTM!public/llms.txt (1)
1-27: LGTM!components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!i18n/locales/en.json (1)
402-402: LGTM!pages/legal.vue (1)
12-20: LGTM!.vscode/settings.json (1)
1-3: LGTM!pages/dmca.vue (1)
5-5: LGTM!Also applies to: 12-20, 49-51
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
pages/privacy-policy.vue (1)
12-20: LGTM!pages/premium/sign-in.vue (1)
8-8: LGTM!Also applies to: 61-70
plugins/040.matomo.client.ts (1)
2-2: LGTM!Also applies to: 41-41, 47-47
components/pages/posts/post/PostTag.vue (1)
2-16: LGTM!Also applies to: 31-31
components/layout/ClientToaster.vue (1)
1-13: LGTM!plugins/050.formbricks.client.ts (1)
2-2: LGTM!Also applies to: 25-25, 31-33
components/layout/navigation/Sidebar.vue (1)
2-4: LGTM!Also applies to: 9-17, 51-51
pages/terms-of-service.vue (1)
5-5: LGTM!Also applies to: 12-20
test/pages/premium/backup.test.ts (1)
25-47: LGTM!Also applies to: 91-91, 98-114
composables/useFaviconUrl.ts (1)
1-7: LGTM!composables/navigation/useMenu.ts (1)
1-1: LGTM!Dockerfile (1)
10-10: LGTM!Also applies to: 15-16
components/shared/ShareButton.vue (1)
13-13: LGTM!components/pages/posts/PostsPageFooter.vue (1)
2-8: LGTM!Also applies to: 125-125
pages/premium/backup.vue (1)
71-79: Duplicate: breadcrumb schema still uses host-relativelocalePath(...)items.This concern was already raised in prior review comments for this file and is still unresolved in the current diff.
pages/other-sites.vue (1)
24-31: Duplicate: breadcrumb schema URLs are still built from host-relativelocalePath(...).This is the same unresolved concern already raised in prior review comments for this file.
i18n/locales/ru.json (1)
402-402: LGTM!components/pages/home/PageHistory.vue (1)
3-3: LGTM!Also applies to: 34-67
components/shared/ContentContainer.vue (1)
2-3: LGTM!Also applies to: 48-57
components/layout/modal/ReviewPrompt.vue (1)
47-49: LGTM!components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-30
assets/js/nuxt-image/imgproxy.provider.ts (1)
41-43: LGTM!Also applies to: 83-83
composables/useLazyToast.ts (1)
1-50: LGTM!composables/useDialogManagerState.ts (1)
1-68: LGTM!components/layout/navigation/Navbar.vue (1)
2-30: LGTM!server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 17-17, 22-22
pages/premium/dashboard.vue (2)
129-137: Use production-origin URLs for breadcrumbitemvalues.
itemvalues are still host-dependent (localePath(...)) instead of canonical production URLs, so structured data can vary by served domain.As per coding guidelines: "For canonical URLs, always point to production (
https://r34.app/…) ... usingproject.urls.production."
19-19: LGTM!Also applies to: 219-221, 262-264, 281-283
pages/index.vue (2)
651-656: Use a canonical production URL for the breadcrumb homeitem.
item: '/'remains host-relative and can produce non-canonical structured data.As per coding guidelines: "For canonical URLs, always point to production (
https://r34.app/…) ... usingproject.urls.production."
124-149: LGTM!Also applies to: 170-173, 783-783, 796-809, 834-835
components/layout/DialogManager.vue (1)
2-77: LGTM!Also applies to: 88-121
layouts/default.vue (1)
2-8: LGTM!Also applies to: 31-35, 43-46, 92-99
components/layout/navigation/SidebarOverlay.vue (1)
1-110: LGTM!test/assets/router-helper.test.ts (1)
24-66: LGTM!pages/premium/tag-collections.vue (1)
3-3: LGTM!Also applies to: 10-11, 147-156, 185-188, 197-199, 219-222, 237-240, 253-256, 266-270, 346-350
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-138
components/pages/posts/post/PostSource.vue (1)
13-13: LGTM!Also applies to: 78-82, 164-164, 173-173, 175-176, 207-207, 209-210, 232-232, 234-235
pages/settings.vue (1)
9-10: LGTM!Also applies to: 32-40
assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-47, 62-71
test/server/fix-canonical-queries.test.ts (1)
14-14: LGTM!Also applies to: 24-24, 33-33, 36-36, 57-57
components/pages/posts/post/PostComponent.vue (1)
2-5: LGTM!Also applies to: 7-7, 15-33, 68-75, 90-90, 174-174, 179-270
pages/premium/saved-posts/[domain].vue (1)
13-13: LGTM!Also applies to: 22-22, 33-39, 91-91, 97-97, 114-117, 232-232, 618-627
components/pages/posts/navigation/DomainSelector.vue (1)
23-24: LGTM!Also applies to: 73-93, 125-148
| const localePath = useLocalePath() | ||
| const { isPremium } = useUserData() | ||
| const { t } = useI18n() | ||
| const { toast } = useLazyToast() |
There was a problem hiding this comment.
Handle useLazyToast() as async at call sites.
toast.error(...) is now async; calling it without await can surface unhandled promise rejections when lazy import fails.
Suggested fix
- function createTagCollectionFromSelectedTags() {
+ async function createTagCollectionFromSelectedTags() {
@@
- toast.error(t('toasts.selectAtLeastOneTag'))
+ await toast.error(t('toasts.selectAtLeastOneTag'))
return
@@
- toast.error(t('toasts.tagCollectionExists'))
+ await toast.error(t('toasts.tagCollectionExists'))
return🤖 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 `@components/pages/posts/navigation/search/TagCollections.vue` at line 17, The
call to useLazyToast() returns a toast whose methods (e.g., toast.error) are
async and must be awaited; update all call sites in TagCollections.vue that call
toast.error (and any other toast.* methods) to await the call and propagate
async by marking the enclosing handler or method async (for example, make the
click/submit handler or the component method async) and handle errors with
try/catch if appropriate so the lazy import failure does not produce unhandled
promise rejections.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.forgotPassword.seoTitle'), item: localePath('/premium/forgot-password') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Use production-absolute URLs for breadcrumb schema items.
These breadcrumb item values are host-relative (localePath(...)), so structured data URL identity changes across domains. Build them from project.urls.production + localized path.
Suggested fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+ const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString()
+ const forgotPasswordBreadcrumbUrl = new URL(localePath('/premium/forgot-password'), project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') },
- { name: t('pages.premium.forgotPassword.seoTitle'), item: localePath('/premium/forgot-password') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl },
+ { name: t('pages.premium.forgotPassword.seoTitle'), item: forgotPasswordBreadcrumbUrl }
]
})
])📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | |
| { name: t('pages.premium.forgotPassword.seoTitle'), item: localePath('/premium/forgot-password') } | |
| ] | |
| }) | |
| ]) | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() | |
| const forgotPasswordBreadcrumbUrl = new URL(localePath('/premium/forgot-password'), project.urls.production).toString() | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, | |
| { name: t('pages.premium.forgotPassword.seoTitle'), item: forgotPasswordBreadcrumbUrl } | |
| ] | |
| }) | |
| ]) |
🤖 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 `@pages/premium/forgot-password.vue` around lines 40 - 48, The breadcrumb
schema is using host-relative URLs via localePath(...) inside
useSchemaOrg/defineBreadcrumb; change the breadcrumb `item` values to
production-absolute URLs built from project.urls.production as the base plus the
localized path (e.g., combine project.urls.production with the result of
localePath('/...') or use new URL(localePath(...), project.urls.production).
Update the three item entries so they use the production base instead of raw
localePath results while keeping the same display names.
db17695 to
30b1ca6
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (6)
composables/useIdleTask.ts (1)
12-12:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse the
timeoutparameter in the fallbacksetTimeoutcall.The fallback on line 12 ignores the
timeoutparameter and always schedules with 1ms delay, creating inconsistency with therequestIdleCallbackpath which respects the timeout.Proposed fix
- window.setTimeout(callback, 1) + window.setTimeout(callback, timeout)🤖 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 `@composables/useIdleTask.ts` at line 12, The fallback branch in composables/useIdleTask.ts currently calls window.setTimeout(callback, 1) and ignores the timeout parameter; update that call to use the provided timeout (e.g., window.setTimeout(callback, timeout)) so the non-requestIdleCallback path respects the same timeout value as the requestIdleCallback path; locate this in the useIdleTask function where the fallback timeout is scheduled and replace the hard-coded 1 with the timeout parameter.plugins/035.sentry.client.ts (1)
46-53:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftReplace the private Sentry deep import with public APIs.
Importing
../node_modules/@sentry/nuxt/build/module/runtime/utils.jsdepends on internal package structure and can break under SDK upgrades or package export changes (especially with pnpm layouts).In `@sentry/nuxt` v10.52.0, is importing from `build/module/runtime/utils.js` (or via a `../node_modules/...` deep path) a supported public API? What documented public API should be used instead to report Nuxt/Vue errors from a client plugin?🤖 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 `@plugins/035.sentry.client.ts` around lines 46 - 53, Remove the fragile deep import of '../node_modules/@sentry/nuxt/build/module/runtime/utils.js' and instead use the public Sentry API: use the imported Sentry (from '`@sentry/nuxt`') and Nuxt's isNuxtError (from 'nuxt/app') and replace the internal reportNuxtError helper by a small local wrapper that detects Nuxt errors via isNuxtError and forwards them to Sentry.captureException (or Sentry.getCurrentHub().captureException) and any init options from '~/sentry.client.options'; update references to reportNuxtError, isNuxtError and buildSentryClientInitOptions accordingly.pages/index.vue (1)
651-656:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse locale-aware home path in breadcrumb schema.
The breadcrumb home item is hardcoded to
'/'; this can mismatch localized routes. UselocalePath('/')here.🤖 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 `@pages/index.vue` around lines 651 - 656, The breadcrumb schema uses a hardcoded home path '/' inside defineBreadcrumb's itemListElement which breaks localized routes; replace the literal '/' with localePath('/') so the home item uses the app's locale-aware path (update the object where name: t('nav.home') and item: '/' to item: localePath('/')). Ensure localePath is imported/available in the scope where defineBreadcrumb is called.pages/tags/[domain]/[tag].vue (1)
30-32:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStop hand-building the
tagsquery string in route construction.Build this as a route object/query (via router helper) instead of string concatenation so Vue Router handles encoding and query shape consistently.
As per coding guidelines: "Query filters use flat bracket keys (filter[sort], filter[rating], etc.) with Vue Router default query handling; do not re-add qs library".
🤖 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 `@pages/tags/`[domain]/[tag].vue around lines 30 - 32, The computed fullPostsRoute currently hand-builds a URL string; change it to construct a route object and let Vue Router encode/query-shape it—for example, use the router resolver (useRouter().resolve or router.resolve) with a route object that sets the route name or params (domain from selectedBooru.value) and query { tags: tagParam.value } and return the resolved .href; update the fullPostsRoute computed to call that resolver instead of string concatenation so encoding and query keys follow Vue Router conventions.nuxt.config.js (2)
197-201:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove Nitro precompression from this deployment profile.
compressPublicAssetsconflicts with the current Cloudflare-first compression guardrail and adds build/runtime complexity without verified payoff for this stack.Based on learnings: "Production is behind Cloudflare with Brotli compression; do not add app-level HTML compression unless verified with headers, byte sizes, warm TTFB, and Lighthouse".
🤖 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 197 - 201, Remove the Nitro precompression config by deleting or disabling the compressPublicAssets block in the nitro config (the compressPublicAssets object currently setting gzip: true and brotli: true) in nuxt.config.js so the app no longer performs app-level gzip/brotli compression; update any related comments or config references, run a local build to verify there are no leftover references to compressPublicAssets, and ensure Cloudflare remains the sole compressing layer.
140-166:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid
immutablecaching on fixed icon filenames.These icon URLs are stable (
/favicon.ico,/icon.svg,/apple-touch-icon-180x180.png, PWA icon paths). Marking them immutable can keep stale icons after deploys unless filenames are versioned.🤖 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 140 - 166, The listed static asset routes ('/icon.svg', '/favicon.ico', '/apple-touch-icon-180x180.png', '/pwa-64x64.png', '/pwa-192x192.png', '/pwa-512x512.png', '/maskable-icon-512x512.png') are currently assigned immutableCacheHeaders which can serve stale icons after deploys; update nuxt.config.js so these fixed filenames use a non-immutable cache policy (e.g., a shorter max-age or revalidate header) instead of immutableCacheHeaders, leaving immutableCacheHeaders only for truly content-addressed/versioned assets; locate the mapping object that assigns headers to these paths and replace the value reference from immutableCacheHeaders to the appropriate non-immutable header constant or inline header object.
🤖 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 `@assets/js/nuxt-image/imgproxy.provider.ts`:
- Line 83: Restore the previous exclusion of width and height from imgproxy
operations by destructuring mergeModifiers to remove width and height before
calling operationsGenerator: extract width and height from mergeModifiers (e.g.,
const { width, height, ...modifiersWithoutSize } = mergeModifiers) and pass
modifiersWithoutSize into operationsGenerator when building the path
(joinURL('/insecure', operationsGenerator(modifiersWithoutSize), encodedUrl)),
so that width/height are not included in the generated imgproxy path.
In `@components/layout/modal/ReviewPrompt.vue`:
- Around line 47-49: The img element that uses
:src="useFaviconUrl('trustpilot.com')" has intrinsic height="64" width="64" that
conflicts with class="h-6 w-6" (24x24) and may cause layout shift; update the
intrinsic attributes on that image to match the rendered size (set height and
width to 24) so the intrinsic dimensions align with the CSS classes used by the
component (ReviewPrompt.vue, the img with
:src="useFaviconUrl('trustpilot.com')").
In `@composables/useFaviconUrl.ts`:
- Around line 3-4: Replace the URL.canParse() + new URL(...) pattern with
URL.parse(normalizedUrl) and use its return value directly: call
URL.parse(normalizedUrl) to obtain the parsed URL (or null), assign it to the
existing url variable, and then compute domain as url?.hostname ?? urlOrDomain;
update usages in composables/useFaviconUrl.ts (variables: url, normalizedUrl,
domain) accordingly so you no longer perform the redundant canParse +
constructor flow.
In `@pages/premium/additional-boorus.vue`:
- Around line 184-187: Breadcrumb items currently use localePath(...) to build
host-relative URLs causing canonical drift; change them to build absolute
production URLs by prefixing the localePath result (or the raw path like
'/premium' etc.) with project.urls.production so the breadcrumb item values are
full production URLs. Locate the breadcrumb array (the entries using
t('nav.home'), t('pages.premium.landingPage.seoTitle'),
t('pages.premium.additionalBoorusPage.seoTitle')) and replace item:
localePath('/...') with item: project.urls.production + localePath('/...') (or
project.urls.production + '/premium' etc.) ensuring project.urls.production is
available/imported in the component.
In `@pages/premium/index.vue`:
- Around line 202-207: The breadcrumb schema currently builds item URLs using
localePath(...) inside defineBreadcrumb/itemListElement which yields
host-dependent URLs; change those item values to use the production base from
project.urls.production (e.g. combine project.urls.production with the path '/''
and '/premium') so the schema emits absolute production URLs rather than
clone-domain localePath values; update the two entries where localePath('/') and
localePath('/premium') are used to construct absolute URLs using
project.urls.production.
In `@pages/premium/saved-posts/`[domain].vue:
- Around line 618-624: Breadcrumb items in the useSchemaOrg call currently use
relative URLs (localePath(...) and route.path) which vary by host; update the
defineBreadcrumb itemListElement so each item builds an absolute production URL
using project.urls.production as the base (e.g., construct each item URL via new
URL(localePath('/...'), project.urls.production).toString() and for the current
route use new URL(route.path, project.urls.production).toString()) so Schema.org
breadcrumb identity always uses the production origin; modify the
useSchemaOrg/defineBreadcrumb invocation to replace localePath(...) and
route.path with these production-absolute URLs.
In `@pages/privacy-policy.vue`:
- Around line 12-17: The breadcrumb schema in useSchemaOrg(() => [
defineBreadcrumb({ itemListElement: [...] }) ]) currently uses localePath(...)
which yields host-relative URLs; replace those with production-absolute URLs by
composing or replacing localePath('/') and localePath('/privacy-policy') with
project.urls.production (e.g. project.urls.production + '/...' or use
project.urls.production plus locale-aware path building) so the generated
structured-data uses https://r34.app/... canonical production URLs for both home
and privacy-policy entries.
In `@pages/settings.vue`:
- Around line 32-39: The breadcrumb Schema.org items use relative URLs via
localePath(...) which makes structured data host-dependent; update the
defineBreadcrumb call inside useSchemaOrg so each item.item is an absolute
production URL by prepending or resolving against project.urls.production.origin
(e.g., build URLs using project.urls.production.origin + localePath('/') and
project.urls.production.origin + localePath('/settings') or use new
URL(localePath(...), project.urls.production.origin). Ensure you update the two
items in defineBreadcrumb and keep the rest of the breadcrumb structure
unchanged.
In `@test/helper.ts`:
- Around line 61-70: The created Page is added to the tracking set too late so
failures in async setup (mockExternalImages or goto) can leak; in
createTrackedPage move the pages.add(page) to immediately after createPage() so
the new Page is tracked before calling mockExternalImages(page) or
page.goto(url(path)); keep the rest of the function the same so cleanup logic
that relies on pages.contains(page) will run even if the subsequent async calls
throw.
In `@test/pages/premium/backup.test.ts`:
- Around line 91-117: The test currently only verifies the file picker opens and
receives a file but no longer asserts the restore flow completes; update the
test (or add a new one) to wait for the restore navigation and assert success:
after await fileChooser.setFiles(...) use page.waitForURL or waitForNavigation
to assert the dashboard URL contains restoreSuccess=true (or the same success
indicator used previously), and/or wait for the same page change used by
createAuthedBackupPage to signal completion; reference the existing symbols
page.waitForEvent('filechooser'), fileChooser.setFiles, and
createAuthedBackupPage to locate where to add the URL/navigation assertion so
the end-to-end restore logic (parsing, application, navigation) is validated.
---
Duplicate comments:
In `@composables/useIdleTask.ts`:
- Line 12: The fallback branch in composables/useIdleTask.ts currently calls
window.setTimeout(callback, 1) and ignores the timeout parameter; update that
call to use the provided timeout (e.g., window.setTimeout(callback, timeout)) so
the non-requestIdleCallback path respects the same timeout value as the
requestIdleCallback path; locate this in the useIdleTask function where the
fallback timeout is scheduled and replace the hard-coded 1 with the timeout
parameter.
In `@nuxt.config.js`:
- Around line 197-201: Remove the Nitro precompression config by deleting or
disabling the compressPublicAssets block in the nitro config (the
compressPublicAssets object currently setting gzip: true and brotli: true) in
nuxt.config.js so the app no longer performs app-level gzip/brotli compression;
update any related comments or config references, run a local build to verify
there are no leftover references to compressPublicAssets, and ensure Cloudflare
remains the sole compressing layer.
- Around line 140-166: The listed static asset routes ('/icon.svg',
'/favicon.ico', '/apple-touch-icon-180x180.png', '/pwa-64x64.png',
'/pwa-192x192.png', '/pwa-512x512.png', '/maskable-icon-512x512.png') are
currently assigned immutableCacheHeaders which can serve stale icons after
deploys; update nuxt.config.js so these fixed filenames use a non-immutable
cache policy (e.g., a shorter max-age or revalidate header) instead of
immutableCacheHeaders, leaving immutableCacheHeaders only for truly
content-addressed/versioned assets; locate the mapping object that assigns
headers to these paths and replace the value reference from
immutableCacheHeaders to the appropriate non-immutable header constant or inline
header object.
In `@pages/index.vue`:
- Around line 651-656: The breadcrumb schema uses a hardcoded home path '/'
inside defineBreadcrumb's itemListElement which breaks localized routes; replace
the literal '/' with localePath('/') so the home item uses the app's
locale-aware path (update the object where name: t('nav.home') and item: '/' to
item: localePath('/')). Ensure localePath is imported/available in the scope
where defineBreadcrumb is called.
In `@pages/tags/`[domain]/[tag].vue:
- Around line 30-32: The computed fullPostsRoute currently hand-builds a URL
string; change it to construct a route object and let Vue Router
encode/query-shape it—for example, use the router resolver (useRouter().resolve
or router.resolve) with a route object that sets the route name or params
(domain from selectedBooru.value) and query { tags: tagParam.value } and return
the resolved .href; update the fullPostsRoute computed to call that resolver
instead of string concatenation so encoding and query keys follow Vue Router
conventions.
In `@plugins/035.sentry.client.ts`:
- Around line 46-53: Remove the fragile deep import of
'../node_modules/@sentry/nuxt/build/module/runtime/utils.js' and instead use the
public Sentry API: use the imported Sentry (from '`@sentry/nuxt`') and Nuxt's
isNuxtError (from 'nuxt/app') and replace the internal reportNuxtError helper by
a small local wrapper that detects Nuxt errors via isNuxtError and forwards them
to Sentry.captureException (or Sentry.getCurrentHub().captureException) and any
init options from '~/sentry.client.options'; update references to
reportNuxtError, isNuxtError and buildSentryClientInitOptions accordingly.
🪄 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: 8a354228-1608-48d9-b979-ad16d846f6a3
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (73)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.ts
💤 Files with no reviewable changes (3)
- app/router.options.ts
- server/plugins/lcp-preload-fetchpriority.ts
- app.vue
📜 Review details
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vue 3 and TypeScript with Nuxt 4 (SSR via Nitro server)
Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals
Files:
components/pages/home/Newsletter.vuecomposables/navigation/useMenu.tscomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/PostsPageFooter.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vueassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tstest/assets/router-helper.test.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuetest/server-mocks/plugin.tspages/other-sites.vuepages/cookie-policy.vuetest/helper.tscomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Navbar.vuetest/pages/premium/backup.test.tsplugins/050.formbricks.client.tscomposables/useIdleTask.tspages/legal.vuecomponents/shared/ShareButton.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vueplugins/035.sentry.client.tscomposables/useLazyToast.tspages/premium/sign-in.vueassets/js/RouterHelper.tscomposables/useInteractionDetector.tstest/server/fix-canonical-queries.test.tscomponents/layout/navigation/SidebarWrapper.vueserver/api/_sitemap-urls.tspages/settings.vuepages/premium/dashboard.vuecomponents/pages/home/PageHistory.vuepages/premium/saved-posts/[domain].vuepages/privacy-policy.vuecomposables/useDialogManagerState.tspages/premium/forgot-password.vuecomponents/layout/DialogManager.vuelayouts/default.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuepages/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuepages/premium/index.vuepages/premium/backup.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/search/TagCollections.vuetest/pages/posts.test.tspages/premium/additional-boorus.vue
**/*.{js,ts,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, single attribute per line in Vue templates
Files:
components/pages/home/Newsletter.vuecomposables/navigation/useMenu.tscomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/PostsPageFooter.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuei18n/locales/ru.jsonassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tstest/assets/router-helper.test.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuetest/server-mocks/plugin.tspages/other-sites.vuepages/cookie-policy.vuetest/helper.tscomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Navbar.vuetest/pages/premium/backup.test.tsplugins/050.formbricks.client.tscomposables/useIdleTask.tsi18n/locales/en.jsonpages/legal.vuecomponents/shared/ShareButton.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vueplugins/035.sentry.client.tscomposables/useLazyToast.tspages/premium/sign-in.vueassets/js/RouterHelper.tsi18n/locales/ja.jsoncomposables/useInteractionDetector.tstest/server/fix-canonical-queries.test.tscomponents/layout/navigation/SidebarWrapper.vuepackage.jsonserver/api/_sitemap-urls.tspages/settings.vuepages/premium/dashboard.vuecomponents/pages/home/PageHistory.vuepages/premium/saved-posts/[domain].vuepages/privacy-policy.vuecomposables/useDialogManagerState.tspages/premium/forgot-password.vuecomponents/layout/DialogManager.vuei18n/locales/es.jsonlayouts/default.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuenuxt.config.jspages/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuepages/premium/index.vuepages/premium/backup.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/search/TagCollections.vuetest/pages/posts.test.tspages/premium/additional-boorus.vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Components are auto-imported without path prefix; import them as not <Folder/ComponentName>
Files:
components/pages/home/Newsletter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Navbar.vuecomponents/shared/ShareButton.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/PageHistory.vuecomponents/layout/DialogManager.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/search/TagCollections.vue
**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Query filters use flat bracket keys (filter[sort], filter[rating], etc.) with Vue Router default query handling; do not re-add qs library
External API calls use NUXT_PUBLIC_API_URL environment variable (default http://localhost:8081)
Files:
components/pages/home/Newsletter.vuecomposables/navigation/useMenu.tscomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/PostsPageFooter.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vueassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tstest/assets/router-helper.test.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuetest/server-mocks/plugin.tspages/other-sites.vuepages/cookie-policy.vuetest/helper.tscomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Navbar.vuetest/pages/premium/backup.test.tsplugins/050.formbricks.client.tscomposables/useIdleTask.tspages/legal.vuecomponents/shared/ShareButton.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vueplugins/035.sentry.client.tscomposables/useLazyToast.tspages/premium/sign-in.vueassets/js/RouterHelper.tscomposables/useInteractionDetector.tstest/server/fix-canonical-queries.test.tscomponents/layout/navigation/SidebarWrapper.vueserver/api/_sitemap-urls.tspages/settings.vuepages/premium/dashboard.vuecomponents/pages/home/PageHistory.vuepages/premium/saved-posts/[domain].vuepages/privacy-policy.vuecomposables/useDialogManagerState.tspages/premium/forgot-password.vuecomponents/layout/DialogManager.vuelayouts/default.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuepages/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuepages/premium/index.vuepages/premium/backup.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/search/TagCollections.vuetest/pages/posts.test.tspages/premium/additional-boorus.vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Images are generated at 1x density only (webp format) via custom imgproxy provider to reduce bandwidth
Use
@nuxt/imagev2 module API for image preload priority instead of patching rendered HTML in Nitro
Files:
components/pages/home/Newsletter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/PostsPageFooter.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostTag.vuepages/other-sites.vuepages/cookie-policy.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Navbar.vuepages/legal.vuecomponents/shared/ShareButton.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Sidebar.vuepages/terms-of-service.vuepages/premium/sign-in.vuecomponents/layout/navigation/SidebarWrapper.vuepages/settings.vuepages/premium/dashboard.vuecomponents/pages/home/PageHistory.vuepages/premium/saved-posts/[domain].vuepages/privacy-policy.vuepages/premium/forgot-password.vuecomponents/layout/DialogManager.vuelayouts/default.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/premium/tag-collections.vuepages/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuepages/premium/index.vuepages/premium/backup.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/additional-boorus.vue
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when parsed object is needed
Files:
composables/navigation/useMenu.tsassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tstest/assets/router-helper.test.tsassets/js/sidebarLinks.tscomposables/useFaviconUrl.tstest/server-mocks/plugin.tstest/helper.tstest/pages/premium/backup.test.tsplugins/050.formbricks.client.tscomposables/useIdleTask.tsplugins/035.sentry.client.tscomposables/useLazyToast.tsassets/js/RouterHelper.tscomposables/useInteractionDetector.tstest/server/fix-canonical-queries.test.tsserver/api/_sitemap-urls.tscomposables/useDialogManagerState.tstest/pages/posts.test.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Page-specific SEO tags (title, description) should use useSeoMeta in the page component
Files:
pages/dmca.vuepages/other-sites.vuepages/cookie-policy.vuepages/legal.vuepages/terms-of-service.vuepages/premium/sign-in.vuepages/settings.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuepages/privacy-policy.vuepages/premium/forgot-password.vuepages/premium/tag-collections.vuepages/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuepages/premium/index.vuepages/premium/backup.vuepages/premium/additional-boorus.vue
i18n/locales/**/*.json
📄 CodeRabbit inference engine (AGENTS.md)
Locales defined in config/i18n.ts: i18n JSON files (en, ru, es, ja) in i18n/locales/
Files:
i18n/locales/ru.jsoni18n/locales/en.jsoni18n/locales/ja.jsoni18n/locales/es.json
plugins/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Plugins are loaded in order by numeric prefix (020., 030., 035., 040., 050.) in plugins/ directory
Files:
plugins/040.matomo.client.tsplugins/050.formbricks.client.tsplugins/035.sentry.client.ts
**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest with
@nuxt/test-utilsand Playwright browser mode for testingTests use
@nuxt/test-utilswith Playwright inside describe blocks calling await setup({ browser: true })For test debug mode, import debugBrowserOptions from test/helper.ts for headful playback with slowMo
Files:
test/assets/router-helper.test.tstest/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
package.json
📄 CodeRabbit inference engine (AGENTS.md)
Node ≥ 24 required (package.json engines)
pnpm ≥ 11.1 required (packageManager pins expected version)
Files:
package.json
server/api/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Server API routes in server/api/ use Nitro conventions
Files:
server/api/_sitemap-urls.ts
Dockerfile
📄 CodeRabbit inference engine (AGENTS.md)
Docker production build: multi-stage with pnpm install --frozen-lockfile; production stage copies only .output/ (no node_modules)
NITRO_PRESET build arg selects deployment target in Docker production build
Files:
Dockerfile
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Prefer high-impact, measurable performance optimizations over small rewrites; verify changes with production build, tests, traces, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Production is behind Cloudflare with Brotli compression; do not add app-level HTML compression unless verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Keep formkit/auto-animate route-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Service worker is intentionally disabled (selfDestroying: true); do not add service worker logic
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Source map uploads only happen in production Docker builds (requires SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN)
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-18T21:38:05.940Z
Learning: Git submodule at assets/lib/rule-34-shared-resources must be cloned with --recursive
🔇 Additional comments (58)
components/pages/home/Newsletter.vue (1)
45-45: LGTM!composables/navigation/useMenu.ts (1)
1-1: LGTM!components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-31
public/llms.txt (1)
1-27: LGTM!components/pages/posts/PostsPageFooter.vue (2)
1-11: LGTM!
125-125: LGTM!pages/dmca.vue (2)
5-5: LGTM!Also applies to: 12-20
49-52: LGTM!components/pages/home/PageHistorySection.vue (1)
1-17: LGTM!i18n/locales/ru.json (1)
404-404: LGTM!assets/js/nuxt-image/imgproxy.provider.ts (1)
41-41: LGTM!test/assets/router-helper.test.ts (1)
1-67: LGTM!.vscode/settings.json (1)
1-4: LGTM!components/layout/ClientToaster.vue (1)
1-14: LGTM!components/pages/posts/post/PostTag.vue (1)
31-31: LGTM!plugins/040.matomo.client.ts (1)
2-2: ⚡ Quick winThe
useIdleTaskcomposable is correctly implemented with proper idle callback scheduling.The implementation properly uses
requestIdleCallbackwith a 2000ms timeout when available, and falls back tosetTimeoutwith a 1ms delay. It also includes client-side checks to prevent SSR issues. No changes needed.assets/js/sidebarLinks.ts (1)
12-56: ⚡ Quick winAll imports have been successfully updated and the hideInStandalone flag is properly handled.
Verification confirms:
- No remaining references to the old
sidebarNavigationexport name- Both
Sidebar.vueandSidebarWrapper.vuecorrectly importsidebarLinks- The
hideInStandaloneflag is properly handled inSidebar.vuewith the computed filter:sidebarLinks.filter((item) => !(item.hideInStandalone && isStandaloneDisplayMode.value))pages/other-sites.vue (1)
24-31: Breadcrumb Schema URLs remain host-relative.Line 27 and Line 28 still use
localePath(...)directly foritem, so structured-data URLs vary by serving host instead of staying canonical.As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
pages/cookie-policy.vue (1)
12-17: Breadcrumb Schema still uses non-canonical item URLs.Line 15 and Line 16 are still host-relative
localePath(...)values, so schema breadcrumb URLs are not pinned to production.As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
components/pages/home/FeaturedTags.vue (2)
103-109:tag.media[0]is still accessed without an empty-array guard.Line 109 still assumes a first media item exists (same pattern appears in the iframe branch), so tags with empty media can still break rendering.
44-46: LGTM!Also applies to: 70-70, 105-108
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-138
test/helper.ts (1)
1-17: LGTM!components/pages/posts/navigation/DomainSelector.vue (1)
23-23: LGTM!Also applies to: 89-92, 143-146
components/shared/ContentContainer.vue (1)
2-3: LGTM!Also applies to: 26-27, 35-36, 48-57
components/layout/navigation/Navbar.vue (1)
2-4: LGTM!Also applies to: 5-30
components/layout/navigation/Sidebar.vue (1)
2-17: LGTM!Also applies to: 51-52
pages/terms-of-service.vue (1)
5-5: LGTM!Also applies to: 12-20
composables/useLazyToast.ts (1)
1-50: LGTM!pages/premium/sign-in.vue (1)
8-8: LGTM!Also applies to: 61-69
assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-46, 62-70
i18n/locales/ja.json (1)
389-390: LGTM!Also applies to: 404-404
pages/premium/dashboard.vue (2)
129-137: Previously flagged issue remains: breadcrumbitemURLs are still relative.These structured-data URLs should be production-absolute, not raw
localePath(...)values.As per coding guidelines:
Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals.
19-19: LGTM!Also applies to: 219-221, 262-264, 281-283
Dockerfile (2)
10-16: Previously flagged issue remains: source-map upload defaults to enabled.The default still enables uploads unless explicitly overridden, which can break non-production/local builds without Sentry credentials.
Based on learnings:
Source map uploads only happen in production Docker builds (requires SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN).
20-23: LGTM!Also applies to: 26-26
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
test/server/fix-canonical-queries.test.ts (1)
14-14: LGTM!Also applies to: 24-24, 33-33, 36-36, 57-57
components/layout/navigation/SidebarWrapper.vue (1)
3-14: LGTM!Also applies to: 17-109
package.json (1)
5-5: LGTM!Also applies to: 9-10
server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 17-17, 22-22
pages/settings.vue (1)
9-10: LGTM!pages/premium/forgot-password.vue (2)
40-46: Duplicate: breadcrumb schema items are still host-relative.This is the same unresolved issue already raised earlier for this file;
itemvalues should be built as production-absolute URLs fromproject.urls.production.
5-5: LGTM!composables/useDialogManagerState.ts (1)
1-65: LGTM!components/layout/DialogManager.vue (1)
2-3: LGTM!Also applies to: 6-7, 12-33, 35-42, 44-67, 69-72, 74-77, 88-88, 100-100, 116-116, 120-120
i18n/locales/es.json (1)
389-390: LGTM!Also applies to: 404-404
layouts/default.vue (1)
2-2: LGTM!Also applies to: 4-4, 6-6, 30-34, 42-45, 91-91, 93-93, 96-96
components/pages/home/PageHistory.vue (1)
35-35:⚠️ Potential issue | 🟠 Major | ⚡ Quick winApply URL.canParse() guard before URL constructor.
The code violates the documented guideline in AGENTS.md: "For URL validation/parsing, prefer
URL.canParse()orURL.parse()over constructortry/catch."Add a
URL.canParse()check before attempting URL construction to avoid exception-driven control flow, while preserving the existing error logging:♻️ Proposed fix
function historyPathToTitle(path: string) { + if (!URL.canParse(path, project.urls.production)) { + return `${t('common.domain')}: ${path}` + } + try { const url = new URL(path, project.urls.production) const lines = [`${t('common.domain')}: ${url.pathname.replace(/^\/posts\//, '')}`]> Likely an incorrect or invalid review comment.components/pages/posts/post/PostSource.vue (1)
13-14: LGTM!Also applies to: 78-82, 164-176, 207-210, 232-235
components/pages/posts/post/PostMedia.vue (1)
12-13: LGTM!Also applies to: 40-67, 120-136, 275-336, 435-438, 532-612, 695-697
pages/premium/tag-collections.vue (1)
3-4: LGTM!Also applies to: 10-12, 147-155, 173-174
pages/posts/[domain].vue (1)
9-14: LGTM!Also applies to: 25-35, 42-47, 74-77, 149-152, 825-830, 987-997, 1167-1167
AGENTS.md (1)
14-19: LGTM!Also applies to: 84-87, 95-123, 152-154
pages/premium/backup.vue (1)
71-79: Breadcrumb item URLs are still host-dependent.At Line 74-76,
itemstill useslocalePath(...)and should be converted to production absolute URLs viaproject.urls.production.components/pages/posts/navigation/search/TagCollections.vue (1)
17-17:useLazyToast()call sites still appear non-awaited.The
toast.error(...)calls tied to lazy toast at Line 39 and Line 51 remain non-awaited.Also applies to: 39-39, 51-51
test/pages/posts.test.ts (1)
475-475: Tag landing assertion is still using a generic image locator.At Line 475,
page.locator('img').first()can pass without confirming post-grid rendering.components/pages/posts/post/PostComponent.vue (1)
2-12: LGTM!Also applies to: 22-39, 76-82, 98-99, 182-183, 187-347
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.additionalBoorusPage.seoTitle'), item: localePath('/premium/additional-boorus') } | ||
| ] |
There was a problem hiding this comment.
Build breadcrumb URLs from project.urls.production.
At Line 184-187, breadcrumb item URLs are relative/host-dependent via localePath(...). Use production absolute URLs to avoid clone-domain canonical drift.
Suggested fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString()
+const additionalBoorusBreadcrumbUrl = new URL(
+ localePath('/premium/additional-boorus'),
+ project.urls.production
+).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') },
- { name: t('pages.premium.additionalBoorusPage.seoTitle'), item: localePath('/premium/additional-boorus') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl },
+ { name: t('pages.premium.additionalBoorusPage.seoTitle'), item: additionalBoorusBreadcrumbUrl }
]
})
])As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals".
🤖 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 `@pages/premium/additional-boorus.vue` around lines 184 - 187, Breadcrumb items
currently use localePath(...) to build host-relative URLs causing canonical
drift; change them to build absolute production URLs by prefixing the localePath
result (or the raw path like '/premium' etc.) with project.urls.production so
the breadcrumb item values are full production URLs. Locate the breadcrumb array
(the entries using t('nav.home'), t('pages.premium.landingPage.seoTitle'),
t('pages.premium.additionalBoorusPage.seoTitle')) and replace item:
localePath('/...') with item: project.urls.production + localePath('/...') (or
project.urls.production + '/premium' etc.) ensuring project.urls.production is
available/imported in the component.
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') } | ||
| ] | ||
| }), |
There was a problem hiding this comment.
Use production absolute URLs in breadcrumb schema.
At Line 204 and Line 205, item is built from localePath(...), which is host-dependent. Build absolute URLs using project.urls.production so schema identity stays stable across clone domains.
Suggested fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }
]
}),As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals".
🤖 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 `@pages/premium/index.vue` around lines 202 - 207, The breadcrumb schema
currently builds item URLs using localePath(...) inside
defineBreadcrumb/itemListElement which yields host-dependent URLs; change those
item values to use the production base from project.urls.production (e.g.
combine project.urls.production with the path '/'' and '/premium') so the schema
emits absolute production URLs rather than clone-domain localePath values;
update the two entries where localePath('/') and localePath('/premium') are used
to construct absolute URLs using project.urls.production.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.savedPostsPage.title'), item: route.path } | ||
| ] |
There was a problem hiding this comment.
Use production-absolute URLs for breadcrumb schema items.
These breadcrumb item URLs are relative, so Schema.org identity changes by host. Build each item with project.urls.production as base.
🔧 Proposed fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString()
+const savedPostsBreadcrumbUrl = new URL(route.path, project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') },
- { name: t('pages.premium.savedPostsPage.title'), item: route.path }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl },
+ { name: t('pages.premium.savedPostsPage.title'), item: savedPostsBreadcrumbUrl }
]
})
])As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | |
| { name: t('pages.premium.savedPostsPage.title'), item: route.path } | |
| ] | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() | |
| const savedPostsBreadcrumbUrl = new URL(route.path, project.urls.production).toString() | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, | |
| { name: t('pages.premium.savedPostsPage.title'), item: savedPostsBreadcrumbUrl } | |
| ] |
🤖 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 `@pages/premium/saved-posts/`[domain].vue around lines 618 - 624, Breadcrumb
items in the useSchemaOrg call currently use relative URLs (localePath(...) and
route.path) which vary by host; update the defineBreadcrumb itemListElement so
each item builds an absolute production URL using project.urls.production as the
base (e.g., construct each item URL via new URL(localePath('/...'),
project.urls.production).toString() and for the current route use new
URL(route.path, project.urls.production).toString()) so Schema.org breadcrumb
identity always uses the production origin; modify the
useSchemaOrg/defineBreadcrumb invocation to replace localePath(...) and
route.path with these production-absolute URLs.
| useSchemaOrg(() => [ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.legal.privacyPolicy'), item: localePath('/privacy-policy') } | ||
| ] |
There was a problem hiding this comment.
Breadcrumb schema URLs should be production-absolute.
localePath(...) here produces host-relative URLs; use project.urls.production to keep structured-data URLs stable across domains.
🔧 Proposed fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const privacyBreadcrumbUrl = new URL(localePath('/privacy-policy'), project.urls.production).toString()
+
useSchemaOrg(() => [
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.legal.privacyPolicy'), item: localePath('/privacy-policy') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.legal.privacyPolicy'), item: privacyBreadcrumbUrl }
]
}),
defineWebPage()
])As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
📝 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.
| useSchemaOrg(() => [ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.legal.privacyPolicy'), item: localePath('/privacy-policy') } | |
| ] | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const privacyBreadcrumbUrl = new URL(localePath('/privacy-policy'), project.urls.production).toString() | |
| useSchemaOrg(() => [ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.legal.privacyPolicy'), item: privacyBreadcrumbUrl } | |
| ] | |
| }), | |
| defineWebPage() | |
| ]) |
🤖 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 `@pages/privacy-policy.vue` around lines 12 - 17, The breadcrumb schema in
useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [...] }) ]) currently
uses localePath(...) which yields host-relative URLs; replace those with
production-absolute URLs by composing or replacing localePath('/') and
localePath('/privacy-policy') with project.urls.production (e.g.
project.urls.production + '/...' or use project.urls.production plus
locale-aware path building) so the generated structured-data uses
https://r34.app/... canonical production URLs for both home and privacy-policy
entries.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.settings.title'), item: localePath('/settings') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Use production-absolute breadcrumb URLs in Schema.org.
item values are currently relative (localePath(...)), so structured data becomes host-dependent on clone domains. Build absolute URLs from project.urls.production.origin.
Proposed fix
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.settings.title'), item: localePath('/settings') }
+ { name: t('nav.home'), item: new URL(localePath('/'), project.urls.production.origin).toString() },
+ { name: t('pages.settings.title'), item: new URL(localePath('/settings'), project.urls.production.origin).toString() }
]
})
])As per coding guidelines: Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals.
📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.settings.title'), item: localePath('/settings') } | |
| ] | |
| }) | |
| ]) | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: new URL(localePath('/'), project.urls.production.origin).toString() }, | |
| { name: t('pages.settings.title'), item: new URL(localePath('/settings'), project.urls.production.origin).toString() } | |
| ] | |
| }) | |
| ]) |
🤖 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 `@pages/settings.vue` around lines 32 - 39, The breadcrumb Schema.org items use
relative URLs via localePath(...) which makes structured data host-dependent;
update the defineBreadcrumb call inside useSchemaOrg so each item.item is an
absolute production URL by prepending or resolving against
project.urls.production.origin (e.g., build URLs using
project.urls.production.origin + localePath('/') and
project.urls.production.origin + localePath('/settings') or use new
URL(localePath(...), project.urls.production.origin). Ensure you update the two
items in defineBreadcrumb and keep the rest of the breadcrumb structure
unchanged.
30b1ca6 to
8e665ea
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
README.md (1)
82-84:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate prerequisites to match the new package manager.
Line 83 still says
NPM, but setup now usespnpm. This can mislead contributors during first-time setup.Suggested fix
### Requirements - NodeJS >= 24 -- NPM +- pnpm (via Corepack or global install)🤖 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 `@README.md` around lines 82 - 84, Update the prerequisites section in README.md: replace the "NPM" entry with "pnpm" (or "pnpm >= <version>" if you want to specify a minimum) so the prerequisites list that currently includes "NodeJS >= 24" correctly reflects the new package manager used by the project; update any nearby install/setup instructions that reference "npm" to use the "pnpm" command names (e.g., pnpm install, pnpm run) to keep README.md consistent.nuxt.config.js (1)
124-135: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winCache the new
/tags/**landing pages too.This PR adds an SSR tag landing page, but
pageRouteRulesstill never covers/tags/**, so those pages fall back to the default headers instead of the 5-minute cache policy used for the other public SEO routes. Adding the route once here is enough — the locale-prefixed mirrors will inherit it automatically.Suggested patch
const pageRouteRules = { // Not prerendered because it needs to redirect old URLs '/': { headers: cacheHeaders }, // `@see` https://github.com/Baroshem/nuxt-security/issues/364 '/posts/**': { security: { xssValidator: false }, headers: cacheHeaders }, + '/tags/**': { headers: cacheHeaders }, // Static pages (prerendered) '/other-sites': { prerender: true, headers: cacheHeaders },🤖 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 124 - 135, The /tags/** SSR landing pages are missing from the routeRules and thus don't get the 5-minute cache headers; add a route entry for '/tags/**' with the same cache headers used by other SEO routes (e.g. 'cache-control': 'public, max-age=300') either inside pageRouteRules or directly in the routeRules object alongside ...pageRouteRules so mirroredRouteRules will inherit it automatically; update the routeRules block where pageRouteRules and mirroredRouteRules are spread to include the '/tags/**' entry.plugins/035.sentry.client.ts (1)
42-83:⚠️ Potential issue | 🟠 Major | ⚡ Quick winClear the init guard if deferred startup fails.
If any dynamic import or
Sentry.init()throws once,initPromisestays rejected forever and the watcher has already stopped, so Sentry never retries for the rest of the session. Keep a separateinitializedflag and clearinitPromiseon failure.Suggested patch
- let initPromise: Promise<void> | null = null + let initPromise: Promise<void> | null = null + let initialized = false @@ async function init() { + if (initialized) return if (initPromise) return initPromise initPromise = (async () => { const [Sentry, { isNuxtError }, { reportNuxtError }, { buildSentryClientInitOptions }] = await Promise.all([ @@ nuxtApp.hook('vue:error', (error, instance, info) => { reportNuxtError({ error, instance, info }) }) - })() + initialized = true + })().catch((error) => { + initPromise = null + throw error + }) return initPromise }🤖 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 `@plugins/035.sentry.client.ts` around lines 42 - 83, The init guard currently leaves initPromise permanently rejected if any dynamic import or Sentry.init() throws; change init() to use a separate boolean (e.g., initialized = false) and wrap the existing async initializer in a try/catch: inside the try perform the Promise.all imports and Sentry.init() and then set initialized = true; in catch clear initPromise (set to null/undefined) before rethrowing or returning the error so future calls can retry; also ensure early-return uses initialized (if true) to avoid re-initializing and that initPromise is only set while the initializer is in-flight (use the same initPromise symbol in the function).
♻️ Duplicate comments (6)
pages/cookie-policy.vue (1)
12-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb structured data.
The breadcrumb
itemvalues currently uselocalePath(...), which generates host-relative URLs that vary by serving domain. Build absolute canonical URLs withproject.urls.productionto maintain a single SEO identity across clone domains.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const cookiePolicyBreadcrumbUrl = new URL(localePath('/cookie-policy'), project.urls.production).toString() + useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.cookiePolicy'), item: localePath('/cookie-policy') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.cookiePolicy'), item: cookiePolicyBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to prevent duplicate content across clone domains.🤖 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 `@pages/cookie-policy.vue` around lines 12 - 20, The breadcrumb structured data created in useSchemaOrg (via defineBreadcrumb) currently uses localePath(...) which yields host-relative URLs; replace those with absolute canonical URLs built from project.urls.production combined with the locale-aware paths so item values are production-absolute. Update the defineBreadcrumb call to construct each item.item using project.urls.production + the same localePath or route path logic (ensuring a leading slash) instead of passing localePath directly, keeping useSchemaOrg and defineWebPage unchanged.pages/settings.vue (1)
32-39:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb structured data.
The breadcrumb
itemvalues currently uselocalePath(...), which generates host-relative URLs that vary by serving domain. Build absolute canonical URLs withproject.urls.productionto maintain a single SEO identity across clone domains.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const settingsBreadcrumbUrl = new URL(localePath('/settings'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.settings.title'), item: localePath('/settings') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.settings.title'), item: settingsBreadcrumbUrl } ] }) ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to prevent duplicate content across clone domains.🤖 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 `@pages/settings.vue` around lines 32 - 39, The breadcrumb structured data currently uses localePath(...) inside the useSchemaOrg([... defineBreadcrumb({...}) ...]) call which produces host-relative URLs; replace those with production-absolute URLs by building each breadcrumb item URL from project.urls.production + the route path (e.g., use project.urls.production for the site origin and append '/' and '/settings' instead of localePath) so defineBreadcrumb's itemListElement items use absolute canonical URLs; update the code referencing useSchemaOrg and defineBreadcrumb to construct items like { name: t('...'), item: project.urls.production + '<path>' } ensuring you import or reference project.urls.production in the same scope.pages/privacy-policy.vue (1)
12-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb structured data.
The breadcrumb
itemvalues currently uselocalePath(...), which generates host-relative URLs that vary by serving domain. Build absolute canonical URLs withproject.urls.productionto maintain a single SEO identity across clone domains.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const privacyBreadcrumbUrl = new URL(localePath('/privacy-policy'), project.urls.production).toString() + useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.privacyPolicy'), item: localePath('/privacy-policy') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.privacyPolicy'), item: privacyBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to prevent duplicate content across clone domains.🤖 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 `@pages/privacy-policy.vue` around lines 12 - 20, The breadcrumb structured data currently uses localePath(...) which yields host-relative URLs; update the defineBreadcrumb call inside useSchemaOrg to build absolute canonical URLs using project.urls.production — replace the item values that call localePath('/') and localePath('/privacy-policy') with production-based absolute URLs (e.g., concatenate project.urls.production with the path) so defineBreadcrumb receives fully qualified URLs; ensure you reference the same symbols (useSchemaOrg, defineBreadcrumb, localePath, project.urls.production) and preserve localization for the name values while switching only the item URL construction to project.urls.production.pages/other-sites.vue (1)
24-31:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb structured data.
The breadcrumb
itemvalues currently uselocalePath(...), which generates host-relative URLs that vary by serving domain. Build absolute canonical URLs withproject.urls.productionto maintain a single SEO identity across clone domains.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const otherSitesBreadcrumbUrl = new URL(localePath('/other-sites'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.otherSites.title'), item: localePath('/other-sites') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.otherSites.title'), item: otherSitesBreadcrumbUrl } ] }) ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to prevent duplicate content across clone domains.🤖 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 `@pages/other-sites.vue` around lines 24 - 31, The breadcrumb structured data currently uses host-relative URLs via localePath inside useSchemaOrg/defineBreadcrumb; change the breadcrumb `item` values to absolute canonical URLs by resolving the localePath output against project.urls.production (e.g., build absolute URLs using project.urls.production as the base) so defineBreadcrumb receives production-absolute strings instead of host-relative paths; update the two items that call localePath('/') and localePath('/other-sites') accordingly within the useSchemaOrg block.composables/useFaviconUrl.ts (1)
3-4:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd browser-safe fallback for
URL.parse(Line 3).
URL.parseis only supported in recent browsers (since September 2024). Provide a fallback usingURL.canParse+new URL, but guard against both being unavailable.♻️ Proposed fix
export function useFaviconUrl(urlOrDomain: string, size = 64) { const normalizedUrl = /^[a-z][a-z\d+\-.]*:\/\//i.test(urlOrDomain) ? urlOrDomain : `https://${urlOrDomain}` - const url = URL.parse(normalizedUrl) + const url = + "parse" in URL + ? URL.parse(normalizedUrl) + : "canParse" in URL && URL.canParse(normalizedUrl) + ? new URL(normalizedUrl) + : null const domain = url?.hostname ?? urlOrDomain return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=${size}` }The original proposed fix calls
URL.canParse()without checking if it exists, which would throw in older browsers where bothURL.parseandURL.canParseare missing. GuardURL.canParsewith a feature check before using it.🤖 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 `@composables/useFaviconUrl.ts` around lines 3 - 4, The code uses URL.parse directly which can throw in older browsers; update the logic that computes url/domain (variables normalizedUrl, urlOrDomain, url, domain) to feature-detect before calling URL.parse or URL.canParse: first check typeof URL?.parse === 'function' and use it; else if typeof URL?.canParse === 'function' and URL.canParse(normalizedUrl) then construct a URL via new URL(normalizedUrl); otherwise attempt a safe try/catch around new URL(normalizedUrl) and fall back to using urlOrDomain for domain if parsing fails—ensure the final domain selection still uses url?.hostname ?? urlOrDomain.plugins/035.sentry.client.ts (1)
46-52:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't deep-import Sentry internals from
node_modules.This path is tied to Sentry's private file layout and your package-manager install layout, so an SDK or pnpm change can break client startup even if the public Nuxt SDK stays compatible. The supported setup initializes from
@sentry/nuxt's package entrypoint and documents public APIs on the top-level SDK object instead. (npmjs.com)🤖 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 `@plugins/035.sentry.client.ts` around lines 46 - 52, Stop the deep import from node_modules and instead use the public exports from the `@sentry/nuxt` package: remove the import of "../node_modules/@sentry/nuxt/build/module/runtime/utils.js" and retrieve isNuxtError, reportNuxtError and buildSentryClientInitOptions from the `@sentry/nuxt` module (or other documented public API) alongside the existing import of '`@sentry/nuxt`'; update the Promise.all array and any call sites that reference isNuxtError, reportNuxtError, or buildSentryClientInitOptions in this file so they use the public properties/methods on the Sentry object rather than a private deep path.
🤖 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 `@assets/js/sidebarLinks.ts`:
- Around line 33-36: Replace the hardcoded FAQ href in the sidebar link object
(the entry with nameKey 'nav.faq') so it uses the canonical production base URL:
build the href from project.urls.production plus the
'/frequently-asked-questions' path (e.g.,
`${project.urls.production}/frequently-asked-questions`) instead of
'https://rule34.app/frequently-asked-questions'; ensure the object (where href
is set and icon is QuestionMarkCircleIcon) still sets isExternal: true and
import/ensure the project config (project.urls.production) is available in the
module before using it.
In `@components/pages/posts/navigation/DomainSelectorFallback.vue`:
- Around line 27-30: The img element using :src="faviconUrl" has intrinsic
height="64" and width="64" that don't match the rendered class="h-5 w-5"
(20×20); update the intrinsic dimensions to match the rendered size (set height
and width to 20) or remove the explicit height/width attributes so the
CSS-controlled h-5/w-5 governs sizing in DomainSelectorFallback.vue to avoid
layout shift.
In `@components/pages/posts/post/PostMedia.vue`:
- Around line 40-43: postImageSizes is currently an object but NuxtPicture's
sizes prop expects a space-separated breakpoint string; replace the object
(postImageSizes) with a string like "sm:400px md:768px" and pass that string to
the NuxtPicture sizes prop, and update the other occurrences where
postImageSizes is used (same symbol references) to use the new string format so
responsive sizing works correctly.
- Around line 291-309: The scheduleVideoPlayerInitialization function schedules
a setTimeout that isn't cleared on component unmount, causing lingering timers;
store the timeout ID (from window.setTimeout) in a variable (e.g.,
videoPlayerInitTimeoutId), clear that timeout in the component's onBeforeUnmount
handler, and reset/clear the ID when the timer runs or when initialization is
canceled so the delayed init is fully canceled when isUnmounted or when
videoPlayer/videoPlayerInitPromise/videoPlayerIdleScheduled become true; update
references to videoPlayerIdleScheduled and initializeVideoPlayer accordingly to
ensure no timers remain after unmount.
In `@pages/index.vue`:
- Around line 651-667: The breadcrumb and search-action structured-data are
using locale-relative URLs (localePath('/')) which can create non-canonical
schema.org links; update the defineBreadcrumb itemListElement[i].item and
defineSearchAction target to build absolute production URLs using new
URL(localePath(...), project.urls.production).toString(), e.g., replace
localePath('/') and '/posts/rule34.xxx?tags={search_term_string}' with new
URL(...) calls so defineBreadcrumb and defineSearchAction emit production
absolute URLs.
In `@pages/premium/sign-in.vue`:
- Around line 61-69: The breadcrumb structured data currently builds item URLs
with localePath(...) which yields host-relative URLs; update the
useSchemaOrg/defineBreadcrumb call to construct absolute canonical URLs by
prefixing each localePath(...) (or replacing it) with project.urls.production so
item values become production absolute URLs (use project.urls.production +
'/premium', '/premium/sign-in', and '/' as appropriate) while keeping existing
labels from t(...).
In `@pages/premium/tag-collections.vue`:
- Around line 147-153: The breadcrumb item URLs passed into
useSchemaOrg/defineBreadcrumb currently use localePath(...) (relative paths);
change them to canonical absolute URLs by prefixing with project.urls.production
(e.g., construct item: new URL(localePath('/premium/tag-collections'),
project.urls.production).toString() or equivalent) for each entry in the
itemListElement array so Schema.org uses production canonical URLs for home,
/premium and /premium/tag-collections.
In `@pages/terms-of-service.vue`:
- Around line 15-16: The breadcrumb items currently use localePath(...) which
produces host-dependent URLs; change the breadcrumb construction to build
canonical URLs using project.urls.production as the base (e.g., replace item:
localePath('/') and item: localePath('/terms-of-service') with items built by
resolving the localePath path against project.urls.production using a URL-safe
join or the URL constructor). Update the code that produces the breadcrumb array
in pages/terms-of-service.vue (references: localePath, t, and the breadcrumb
variable/array) to import or reference project.urls.production and use it as the
base for generating item values, ensuring proper handling of leading/trailing
slashes so the final item strings are absolute production URLs.
---
Outside diff comments:
In `@nuxt.config.js`:
- Around line 124-135: The /tags/** SSR landing pages are missing from the
routeRules and thus don't get the 5-minute cache headers; add a route entry for
'/tags/**' with the same cache headers used by other SEO routes (e.g.
'cache-control': 'public, max-age=300') either inside pageRouteRules or directly
in the routeRules object alongside ...pageRouteRules so mirroredRouteRules will
inherit it automatically; update the routeRules block where pageRouteRules and
mirroredRouteRules are spread to include the '/tags/**' entry.
In `@plugins/035.sentry.client.ts`:
- Around line 42-83: The init guard currently leaves initPromise permanently
rejected if any dynamic import or Sentry.init() throws; change init() to use a
separate boolean (e.g., initialized = false) and wrap the existing async
initializer in a try/catch: inside the try perform the Promise.all imports and
Sentry.init() and then set initialized = true; in catch clear initPromise (set
to null/undefined) before rethrowing or returning the error so future calls can
retry; also ensure early-return uses initialized (if true) to avoid
re-initializing and that initPromise is only set while the initializer is
in-flight (use the same initPromise symbol in the function).
In `@README.md`:
- Around line 82-84: Update the prerequisites section in README.md: replace the
"NPM" entry with "pnpm" (or "pnpm >= <version>" if you want to specify a
minimum) so the prerequisites list that currently includes "NodeJS >= 24"
correctly reflects the new package manager used by the project; update any
nearby install/setup instructions that reference "npm" to use the "pnpm" command
names (e.g., pnpm install, pnpm run) to keep README.md consistent.
---
Duplicate comments:
In `@composables/useFaviconUrl.ts`:
- Around line 3-4: The code uses URL.parse directly which can throw in older
browsers; update the logic that computes url/domain (variables normalizedUrl,
urlOrDomain, url, domain) to feature-detect before calling URL.parse or
URL.canParse: first check typeof URL?.parse === 'function' and use it; else if
typeof URL?.canParse === 'function' and URL.canParse(normalizedUrl) then
construct a URL via new URL(normalizedUrl); otherwise attempt a safe try/catch
around new URL(normalizedUrl) and fall back to using urlOrDomain for domain if
parsing fails—ensure the final domain selection still uses url?.hostname ??
urlOrDomain.
In `@pages/cookie-policy.vue`:
- Around line 12-20: The breadcrumb structured data created in useSchemaOrg (via
defineBreadcrumb) currently uses localePath(...) which yields host-relative
URLs; replace those with absolute canonical URLs built from
project.urls.production combined with the locale-aware paths so item values are
production-absolute. Update the defineBreadcrumb call to construct each
item.item using project.urls.production + the same localePath or route path
logic (ensuring a leading slash) instead of passing localePath directly, keeping
useSchemaOrg and defineWebPage unchanged.
In `@pages/other-sites.vue`:
- Around line 24-31: The breadcrumb structured data currently uses host-relative
URLs via localePath inside useSchemaOrg/defineBreadcrumb; change the breadcrumb
`item` values to absolute canonical URLs by resolving the localePath output
against project.urls.production (e.g., build absolute URLs using
project.urls.production as the base) so defineBreadcrumb receives
production-absolute strings instead of host-relative paths; update the two items
that call localePath('/') and localePath('/other-sites') accordingly within the
useSchemaOrg block.
In `@pages/privacy-policy.vue`:
- Around line 12-20: The breadcrumb structured data currently uses
localePath(...) which yields host-relative URLs; update the defineBreadcrumb
call inside useSchemaOrg to build absolute canonical URLs using
project.urls.production — replace the item values that call localePath('/') and
localePath('/privacy-policy') with production-based absolute URLs (e.g.,
concatenate project.urls.production with the path) so defineBreadcrumb receives
fully qualified URLs; ensure you reference the same symbols (useSchemaOrg,
defineBreadcrumb, localePath, project.urls.production) and preserve localization
for the name values while switching only the item URL construction to
project.urls.production.
In `@pages/settings.vue`:
- Around line 32-39: The breadcrumb structured data currently uses
localePath(...) inside the useSchemaOrg([... defineBreadcrumb({...}) ...]) call
which produces host-relative URLs; replace those with production-absolute URLs
by building each breadcrumb item URL from project.urls.production + the route
path (e.g., use project.urls.production for the site origin and append '/' and
'/settings' instead of localePath) so defineBreadcrumb's itemListElement items
use absolute canonical URLs; update the code referencing useSchemaOrg and
defineBreadcrumb to construct items like { name: t('...'), item:
project.urls.production + '<path>' } ensuring you import or reference
project.urls.production in the same scope.
In `@plugins/035.sentry.client.ts`:
- Around line 46-52: Stop the deep import from node_modules and instead use the
public exports from the `@sentry/nuxt` package: remove the import of
"../node_modules/@sentry/nuxt/build/module/runtime/utils.js" and retrieve
isNuxtError, reportNuxtError and buildSentryClientInitOptions from the
`@sentry/nuxt` module (or other documented public API) alongside the existing
import of '`@sentry/nuxt`'; update the Promise.all array and any call sites that
reference isNuxtError, reportNuxtError, or buildSentryClientInitOptions in this
file so they use the public properties/methods on the Sentry object rather than
a private deep path.
🪄 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: f4e38a24-fe61-4343-bf5e-22d817109335
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (73)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsi18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/ja.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.ts
💤 Files with no reviewable changes (3)
- app.vue
- server/plugins/lcp-preload-fetchpriority.ts
- app/router.options.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vue 3 + TypeScript with Nuxt 4 (SSR, Nitro server) for the application stack
Import components as not <Input/DomainSelector> due to flat auto-import configuration
Use flat bracket keys (filter[sort], filter[rating], etc.) for query filters with Vue Router default handling
Files:
composables/navigation/useMenu.tscomponents/layout/modal/ReviewPrompt.vuepages/legal.vuecomponents/layout/ClientToaster.vuepages/dmca.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuepages/cookie-policy.vuepages/other-sites.vueserver/api/_sitemap-urls.tsassets/js/nuxt-image/imgproxy.provider.tspages/privacy-policy.vuepages/settings.vuecomponents/layout/navigation/Sidebar.vueplugins/040.matomo.client.tstest/helper.tstest/assets/router-helper.test.tscomponents/pages/home/PageHistory.vuecomposables/useIdleTask.tspages/premium/forgot-password.vueplugins/050.formbricks.client.tscomponents/shared/ContentContainer.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuepages/premium/dashboard.vuecomposables/useInteractionDetector.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/Newsletter.vuecomposables/useLazyToast.tscomponents/pages/home/PageHistorySection.vuetest/pages/premium/backup.test.tsassets/js/sidebarLinks.tspages/premium/tag-collections.vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuepages/terms-of-service.vuecomponents/pages/posts/post/PostComponent.vuecomponents/layout/navigation/SidebarWrapper.vuetest/server-mocks/plugin.tspages/premium/saved-posts/[domain].vueassets/js/RouterHelper.tscomposables/useDialogManagerState.tspages/premium/additional-boorus.vuetest/server/fix-canonical-queries.test.tspages/index.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/layout/DialogManager.vuepages/premium/index.vuecomponents/pages/home/FeaturedTags.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuecomponents/pages/posts/post/PostMedia.vueplugins/035.sentry.client.tstest/pages/posts.test.ts
{config/**/*.ts,**/*.{vue,ts,tsx}}
📄 CodeRabbit inference engine (AGENTS.md)
Use project.urls.production for canonical URLs to prevent duplicate content across clone domains
Files:
composables/navigation/useMenu.tscomponents/layout/modal/ReviewPrompt.vuepages/legal.vuecomponents/layout/ClientToaster.vuepages/dmca.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuepages/cookie-policy.vuepages/other-sites.vueserver/api/_sitemap-urls.tsassets/js/nuxt-image/imgproxy.provider.tspages/privacy-policy.vuepages/settings.vuecomponents/layout/navigation/Sidebar.vueplugins/040.matomo.client.tstest/helper.tstest/assets/router-helper.test.tscomponents/pages/home/PageHistory.vuecomposables/useIdleTask.tspages/premium/forgot-password.vueplugins/050.formbricks.client.tscomponents/shared/ContentContainer.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuepages/premium/dashboard.vuecomposables/useInteractionDetector.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/Newsletter.vuecomposables/useLazyToast.tscomponents/pages/home/PageHistorySection.vuetest/pages/premium/backup.test.tsassets/js/sidebarLinks.tspages/premium/tag-collections.vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuepages/terms-of-service.vuecomponents/pages/posts/post/PostComponent.vuecomponents/layout/navigation/SidebarWrapper.vuetest/server-mocks/plugin.tspages/premium/saved-posts/[domain].vueassets/js/RouterHelper.tscomposables/useDialogManagerState.tspages/premium/additional-boorus.vuetest/server/fix-canonical-queries.test.tspages/index.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/layout/DialogManager.vuepages/premium/index.vuecomponents/pages/home/FeaturedTags.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuecomponents/pages/posts/post/PostMedia.vueplugins/035.sentry.client.tstest/pages/posts.test.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer URL.canParse() or URL.parse() over constructor try/catch for URL validation/parsing
Use URL.parse() when parsed URL object is needed, with URL.canParse() fallback in browser code for compatibility
Files:
composables/navigation/useMenu.tsserver/api/_sitemap-urls.tsassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tstest/helper.tstest/assets/router-helper.test.tscomposables/useIdleTask.tsplugins/050.formbricks.client.tscomposables/useFaviconUrl.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tstest/pages/premium/backup.test.tsassets/js/sidebarLinks.tstest/server-mocks/plugin.tsassets/js/RouterHelper.tscomposables/useDialogManagerState.tstest/server/fix-canonical-queries.test.tsnuxt.config.jsplugins/035.sentry.client.tstest/pages/posts.test.ts
**/*.{vue,ts,tsx,js,json,css,md}
📄 CodeRabbit inference engine (AGENTS.md)
Configure Prettier with 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates
Files:
composables/navigation/useMenu.tscomponents/layout/modal/ReviewPrompt.vuepages/legal.vuecomponents/layout/ClientToaster.vuepages/dmca.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuei18n/locales/es.jsonpages/cookie-policy.vuepages/other-sites.vueserver/api/_sitemap-urls.tsassets/js/nuxt-image/imgproxy.provider.tspages/privacy-policy.vuepages/settings.vuecomponents/layout/navigation/Sidebar.vueplugins/040.matomo.client.tstest/helper.tstest/assets/router-helper.test.tscomponents/pages/home/PageHistory.vuecomposables/useIdleTask.tspages/premium/forgot-password.vueplugins/050.formbricks.client.tsi18n/locales/ja.jsoncomponents/shared/ContentContainer.vuei18n/locales/ru.jsoncomposables/useFaviconUrl.tscomponents/pages/posts/post/PostTag.vuepages/premium/dashboard.vuei18n/locales/en.jsoncomposables/useInteractionDetector.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/Newsletter.vuecomposables/useLazyToast.tscomponents/pages/home/PageHistorySection.vuetest/pages/premium/backup.test.tsassets/js/sidebarLinks.tspages/premium/tag-collections.vueREADME.mdcomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuepages/terms-of-service.vuecomponents/pages/posts/post/PostComponent.vuecomponents/layout/navigation/SidebarWrapper.vuetest/server-mocks/plugin.tspages/premium/saved-posts/[domain].vueassets/js/RouterHelper.tscomposables/useDialogManagerState.tspackage.jsonpages/premium/additional-boorus.vuetest/server/fix-canonical-queries.test.tsAGENTS.mdpages/index.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/layout/DialogManager.vuepages/premium/index.vuecomponents/pages/home/FeaturedTags.vuepages/tags/[domain]/[tag].vuenuxt.config.jspages/posts/[domain].vuecomponents/pages/posts/post/PostMedia.vueplugins/035.sentry.client.tstest/pages/posts.test.ts
{nuxt.config.ts,**/*.vue}
📄 CodeRabbit inference engine (AGENTS.md)
Generate images at 1x density only in webp format to reduce bandwidth
Use
@nuxt/imagemodule API for image preload priority instead of patching rendered HTML in Nitro
Files:
components/layout/modal/ReviewPrompt.vuepages/legal.vuecomponents/layout/ClientToaster.vuepages/dmca.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuepages/cookie-policy.vuepages/other-sites.vuepages/privacy-policy.vuepages/settings.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/home/PageHistory.vuepages/premium/forgot-password.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuepages/premium/dashboard.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistorySection.vuepages/premium/tag-collections.vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuepages/terms-of-service.vuecomponents/pages/posts/post/PostComponent.vuecomponents/layout/navigation/SidebarWrapper.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/index.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/layout/DialogManager.vuepages/premium/index.vuecomponents/pages/home/FeaturedTags.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vuecomponents/pages/posts/post/PostMedia.vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use imgproxy for SSR post images including local development; validate image delivery where imgproxy can resolve source URL
Files:
components/layout/modal/ReviewPrompt.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/home/PageHistory.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistorySection.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/post/PostComponent.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/layout/DialogManager.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostMedia.vue
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use useSeoMeta in page components for page-specific SEO tags (title, description)
Keep
@formkit/auto-animateroute-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages
Files:
pages/legal.vuepages/dmca.vuepages/premium/sign-in.vuepages/cookie-policy.vuepages/other-sites.vuepages/privacy-policy.vuepages/settings.vuepages/premium/forgot-password.vuepages/premium/dashboard.vuepages/premium/tag-collections.vuepages/premium/backup.vuepages/terms-of-service.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/index.vuepages/premium/index.vuepages/tags/[domain]/[tag].vuepages/posts/[domain].vue
test/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright inside describe blocks that call await setup({ browser: true })Import debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo in test debugging
Files:
test/assets/router-helper.test.tstest/pages/premium/backup.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
test/server-mocks/plugin.ts
📄 CodeRabbit inference engine (AGENTS.md)
Mock server-side API calls via test-only Nitro plugin at test/server-mocks/plugin.ts
Files:
test/server-mocks/plugin.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Do not re-add qs for nested route.query.filter objects unless URL contract changes
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Prefer high-impact, measurable performance optimizations over small rewrites
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Verify substantial performance changes with production build, tests, request traces, and Lighthouse before keeping changes
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Do not add app-level HTML compression unless direct-origin deployment needs it with verified headers, byte sizes, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Keep global TanStack Vue Query plugin unless larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Upload source maps to Sentry only in production Docker builds with SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Use multi-stage Docker build with pnpm install --frozen-lockfile in build stage
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Set SENTRY_UPLOAD_SOURCE_MAPS=false to skip source map uploads if SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN are unavailable
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Copy only .output/ in Docker production stage; node_modules not needed as Nitro bundles everything
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T07:31:00.426Z
Learning: Use NITRO_PRESET build arg to select deployment target in Docker production build
🔇 Additional comments (56)
components/layout/modal/ReviewPrompt.vue (1)
47-49: Past review comment addressed.The intrinsic image dimensions now correctly match the rendered size (24×24 pixels aligns with
class="h-6 w-6").composables/navigation/useMenu.ts (1)
1-1: LGTM!.vscode/settings.json (1)
1-3: LGTM!pnpm-workspace.yaml (1)
1-6: LGTM!pages/legal.vue (1)
12-20: LGTM!components/layout/ClientToaster.vue (1)
1-13: LGTM!pages/dmca.vue (2)
12-20: LGTM!
49-51: LGTM!pages/premium/sign-in.vue (1)
8-8: LGTM!i18n/locales/es.json (1)
389-390: LGTM!Also applies to: 404-404
server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 22-22
pages/settings.vue (1)
9-9: LGTM!pages/premium/forgot-password.vue (2)
40-46: Breadcrumb schema items should use production-absolute URLs.
itemvalues are still host-relative (localePath(...)) and should be built fromproject.urls.productionto keep structured-data URL identity stable across clone domains.
As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains."
5-5: LGTM!components/layout/navigation/Sidebar.vue (1)
2-4: LGTM!Also applies to: 9-17, 51-51
plugins/040.matomo.client.ts (1)
2-2: LGTM!Also applies to: 41-42, 47-47
test/helper.ts (1)
1-1: LGTM!Also applies to: 4-17, 62-69
test/assets/router-helper.test.ts (1)
1-2: LGTM!Also applies to: 24-66
components/pages/home/PageHistory.vue (1)
3-3: LGTM!Also applies to: 35-35
composables/useIdleTask.ts (1)
1-16: LGTM!plugins/050.formbricks.client.ts (1)
2-2: LGTM!Also applies to: 25-25, 31-33
pages/premium/dashboard.vue (2)
129-137: Use production-origin absolute URLs for breadcrumbitemfields.
defineBreadcrumbstill uses relativelocalePath(...)values foritem, which keeps schema host-dependent on clone domains.As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains."
19-20: LGTM!Also applies to: 219-221, 262-264, 281-283
i18n/locales/ja.json (1)
389-390: LGTM!Also applies to: 404-404
components/shared/ContentContainer.vue (1)
2-2: LGTM!Also applies to: 26-26, 35-35, 48-57
i18n/locales/ru.json (1)
389-390: LGTM!Also applies to: 404-404
components/pages/posts/post/PostTag.vue (1)
2-15: LGTM!Also applies to: 16-16, 31-31
i18n/locales/en.json (1)
389-390: LGTM!Also applies to: 404-404
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
components/layout/navigation/Navbar.vue (1)
2-3: LGTM!Also applies to: 5-30
components/pages/home/Newsletter.vue (1)
45-45: LGTM!public/llms.txt (1)
1-28: LGTM!composables/useLazyToast.ts (1)
1-50: LGTM!components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!test/pages/premium/backup.test.ts (1)
25-46: LGTM!Also applies to: 91-117, 126-144
assets/js/sidebarLinks.ts (1)
12-31: LGTM!Also applies to: 39-55
pages/premium/tag-collections.vue (1)
3-3: LGTM!Also applies to: 10-12, 185-188, 197-199, 219-222, 237-240, 253-256, 266-270, 346-350
Dockerfile (2)
10-16: Keep source-map uploads disabled by default in Docker builds.This still defaults to
SENTRY_UPLOAD_SOURCE_MAPS=true, which reintroduces the previously flagged behavior for non-production/credential-less builds.Based on learnings: "Set SENTRY_UPLOAD_SOURCE_MAPS=false to skip source map uploads if SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN are unavailable".
20-23: LGTM!Also applies to: 26-26
pages/premium/backup.vue (2)
71-77: Use production absolute URLs for breadcrumbitemvalues.
localePath(...)makes schema URLs host-dependent; breadcrumb items should be built fromproject.urls.productionfor stable canonical identity.As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains".
9-10: LGTM!Also applies to: 63-65, 127-127
components/shared/ShareButton.vue (1)
11-14: LGTM!layouts/default.vue (1)
2-7: LGTM!Also applies to: 30-34, 91-97
components/pages/posts/post/PostComponent.vue (1)
2-39: LGTM!Also applies to: 76-83, 98-99, 182-183, 187-347
components/layout/navigation/SidebarWrapper.vue (1)
5-10: LGTM!Also applies to: 17-24, 96-105
pages/premium/saved-posts/[domain].vue (2)
618-624: Use production-absolute breadcrumb item URLs.This is the same unresolved issue from earlier review: breadcrumb
itemvalues on Line 621-623 are host-relative and can drift across clone domains.As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
13-13: LGTM!Also applies to: 22-22, 33-38, 91-91, 97-97, 114-117, 232-232
pages/premium/additional-boorus.vue (2)
181-187: Build breadcrumbitemURLs with the production origin.This is the same unresolved issue already reported earlier: Line 184-186 still uses host-relative breadcrumb URLs.
As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even on clone domains; use project.urls.production for canonicals."
4-4: LGTM!Also applies to: 11-13, 206-209, 213-215, 234-237, 243-246, 266-269, 285-288, 301-304, 314-318, 421-425
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-138
assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-46, 62-69
composables/useDialogManagerState.ts (1)
1-65: LGTM!package.json (1)
5-5: LGTM!Also applies to: 9-10
test/server/fix-canonical-queries.test.ts (1)
14-14: LGTM!Also applies to: 24-24, 33-33, 36-36, 57-57
AGENTS.md (1)
8-8: LGTM!Also applies to: 14-14, 18-18, 25-33, 39-40, 68-68, 84-87, 95-123, 152-154
pages/premium/index.vue (1)
202-206: Still using locale-relative breadcrumb item URLs in Schema.org.This is the same previously reported issue: breadcrumb
itemshould use absolute production URLs derived fromproject.urls.productioninstead oflocalePath(...)values.
| nameKey: 'nav.faq', | ||
| icon: QuestionMarkCircleIcon, | ||
| href: 'https://rule34.app/frequently-asked-questions', | ||
| isExternal: true | ||
| isExternal: true, | ||
| icon: QuestionMarkCircleIcon |
There was a problem hiding this comment.
Use project.urls.production for the FAQ URL instead of a hardcoded domain.
This link still points to a fixed host, which can produce non-canonical navigation on clone domains.
Suggested fix
{
nameKey: 'nav.faq',
- href: 'https://rule34.app/frequently-asked-questions',
+ href: new URL('frequently-asked-questions', project.urls.production).toString(),
isExternal: true,
icon: QuestionMarkCircleIcon
},As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains".
📝 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.
| nameKey: 'nav.faq', | |
| icon: QuestionMarkCircleIcon, | |
| href: 'https://rule34.app/frequently-asked-questions', | |
| isExternal: true | |
| isExternal: true, | |
| icon: QuestionMarkCircleIcon | |
| nameKey: 'nav.faq', | |
| href: new URL('frequently-asked-questions', project.urls.production).toString(), | |
| isExternal: true, | |
| icon: QuestionMarkCircleIcon |
🤖 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 `@assets/js/sidebarLinks.ts` around lines 33 - 36, Replace the hardcoded FAQ
href in the sidebar link object (the entry with nameKey 'nav.faq') so it uses
the canonical production base URL: build the href from project.urls.production
plus the '/frequently-asked-questions' path (e.g.,
`${project.urls.production}/frequently-asked-questions`) instead of
'https://rule34.app/frequently-asked-questions'; ensure the object (where href
is set and icon is QuestionMarkCircleIcon) still sets isExternal: true and
import/ensure the project config (project.urls.production) is available in the
module before using it.
| const postImageSizes = { | ||
| sm: '400px', | ||
| md: '768px' | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Read the specific file and lines mentioned in the review
cat -n components/pages/posts/post/PostMedia.vue | sed -n '35,50p'Repository: Rule-34/App
Length of output: 1068
🏁 Script executed:
# Also check the usage locations
cat -n components/pages/posts/post/PostMedia.vue | sed -n '540,575p'Repository: Rule-34/App
Length of output: 1457
🏁 Script executed:
# Check if `@nuxt/image` is a dependency and what version
cat package.json | grep -A2 -B2 '`@nuxt/image`'Repository: Rule-34/App
Length of output: 222
🌐 Web query:
@nuxt/image sizes prop format specification
💡 Result:
In @nuxt/image, the prop sizes takes the same string format as the HTML img sizes attribute, described as “a space-separated list of screen size/width pairs”. Example: sizes="100vw sm:50vw md:400px". If you omit a screen size prefix (e.g. no sm:), that size is the “default” size until the next specified screen width; with a prefix (e.g. md:400px), the width is used for that breakpoint and up. [1] So the specification/grammar is: - Parts are separated by spaces. [1] - Each part is either: - A default width (no prefix), or [1] - A breakpoint prefix + width, in the form : (e.g. sm:50vw, md:400px). [1] - Nuxt can also use configured screen names (the module has predefined screen sizes like sm, md, lg, xl, 2xl) when you write these breakpoint prefixes. [2] Note: There have been issues discussing that presets may not support sizes cleanly, but the prop itself follows the above format when used directly on . [3][4]
Citations:
- 1: https://image.nuxt.com/usage/nuxt-img
- 2: https://image.nuxt.com/get-started/configuration
- 3:
sizesis not supported in presets (missing feature/documentation) nuxt/image#734 - 4: Sizes should be optional nuxt/image#458
Change sizes to a space-separated string format.
postImageSizes is passed to the sizes prop of NuxtPicture, but @nuxt/image expects a space-separated string in the format sm:400px md:768px, not a breakpoint object. The current object will not produce responsive sizing as intended.
- const postImageSizes = {
- sm: '400px',
- md: '768px'
- }
+ const postImageSizes = 'sm:400px md:768px'Also applies to: 543–544, 570
🤖 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 `@components/pages/posts/post/PostMedia.vue` around lines 40 - 43,
postImageSizes is currently an object but NuxtPicture's sizes prop expects a
space-separated breakpoint string; replace the object (postImageSizes) with a
string like "sm:400px md:768px" and pass that string to the NuxtPicture sizes
prop, and update the other occurrences where postImageSizes is used (same symbol
references) to use the new string format so responsive sizing works correctly.
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { | ||
| name: t('nav.home'), | ||
| item: localePath('/') | ||
| } | ||
| ] | ||
| }), | ||
|
|
||
| defineWebSite({ | ||
| name: project.name, | ||
|
|
||
| potentialAction: [ | ||
| // TODO: Listen to router | ||
| defineSearchAction({ | ||
| target: '/?query={search_term_string}' | ||
| target: '/posts/rule34.xxx?tags={search_term_string}' | ||
| }) |
There was a problem hiding this comment.
Use production absolute URLs in structured data URLs.
defineBreadcrumb.item and defineSearchAction.target are currently locale-relative. Build these with new URL(localePath(...), project.urls.production).toString() so Schema.org URLs stay canonical across clone domains.
Suggested patch
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const searchTargetUrl = new URL('/posts/rule34.xxx?tags={search_term_string}', project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
{
name: t('nav.home'),
- item: localePath('/')
+ item: homeBreadcrumbUrl
}
]
}),
defineWebSite({
name: project.name,
potentialAction: [
defineSearchAction({
- target: '/posts/rule34.xxx?tags={search_term_string}'
+ target: searchTargetUrl
})
]
}),As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains".
🤖 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 `@pages/index.vue` around lines 651 - 667, The breadcrumb and search-action
structured-data are using locale-relative URLs (localePath('/')) which can
create non-canonical schema.org links; update the defineBreadcrumb
itemListElement[i].item and defineSearchAction target to build absolute
production URLs using new URL(localePath(...),
project.urls.production).toString(), e.g., replace localePath('/') and
'/posts/rule34.xxx?tags={search_term_string}' with new URL(...) calls so
defineBreadcrumb and defineSearchAction emit production absolute URLs.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.signIn.seoTitle'), item: localePath('/premium/sign-in') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Use production-absolute URLs for breadcrumb structured data.
The breadcrumb item values currently use localePath(...), which generates host-relative URLs that vary by serving domain. Build absolute canonical URLs with project.urls.production to maintain a single SEO identity across clone domains.
🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+ const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString()
+ const signInBreadcrumbUrl = new URL(localePath('/premium/sign-in'), project.urls.production).toString()
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') },
- { name: t('pages.premium.signIn.seoTitle'), item: localePath('/premium/sign-in') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl },
+ { name: t('pages.premium.signIn.seoTitle'), item: signInBreadcrumbUrl }
]
})
])As per coding guidelines: Use project.urls.production for canonical URLs to prevent duplicate content across clone domains.
📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | |
| { name: t('pages.premium.signIn.seoTitle'), item: localePath('/premium/sign-in') } | |
| ] | |
| }) | |
| ]) | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() | |
| const signInBreadcrumbUrl = new URL(localePath('/premium/sign-in'), project.urls.production).toString() | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, | |
| { name: t('pages.premium.signIn.seoTitle'), item: signInBreadcrumbUrl } | |
| ] | |
| }) | |
| ]) |
🤖 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 `@pages/premium/sign-in.vue` around lines 61 - 69, The breadcrumb structured
data currently builds item URLs with localePath(...) which yields host-relative
URLs; update the useSchemaOrg/defineBreadcrumb call to construct absolute
canonical URLs by prefixing each localePath(...) (or replacing it) with
project.urls.production so item values become production absolute URLs (use
project.urls.production + '/premium', '/premium/sign-in', and '/' as
appropriate) while keeping existing labels from t(...).
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: localePath('/premium/tag-collections') } | ||
| ] |
There was a problem hiding this comment.
Use canonical absolute URLs in Schema.org breadcrumbs.
item values are currently relative paths; for canonical consistency across domains, build absolute URLs from project.urls.production.
Suggested fix
import Slideover from '~/components/layout/Slideover.vue'
+ import { project } from '`@/config/project`'
@@
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') },
- { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: localePath('/premium/tag-collections') }
+ { name: t('nav.home'), item: new URL(localePath('/'), project.urls.production).toString() },
+ { name: t('pages.premium.landingPage.seoTitle'), item: new URL(localePath('/premium'), project.urls.production).toString() },
+ { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: new URL(localePath('/premium/tag-collections'), project.urls.production).toString() }
]
})
])As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains".
📝 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.
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | |
| { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: localePath('/premium/tag-collections') } | |
| ] | |
| import Slideover from '~/components/layout/Slideover.vue' | |
| import { project } from '`@/config/project`' | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: new URL(localePath('/'), project.urls.production).toString() }, | |
| { name: t('pages.premium.landingPage.seoTitle'), item: new URL(localePath('/premium'), project.urls.production).toString() }, | |
| { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: new URL(localePath('/premium/tag-collections'), project.urls.production).toString() } | |
| ] | |
| }) | |
| ]) |
🤖 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 `@pages/premium/tag-collections.vue` around lines 147 - 153, The breadcrumb
item URLs passed into useSchemaOrg/defineBreadcrumb currently use
localePath(...) (relative paths); change them to canonical absolute URLs by
prefixing with project.urls.production (e.g., construct item: new
URL(localePath('/premium/tag-collections'), project.urls.production).toString()
or equivalent) for each entry in the itemListElement array so Schema.org uses
production canonical URLs for home, /premium and /premium/tag-collections.
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.legal.termsOfService'), item: localePath('/terms-of-service') } |
There was a problem hiding this comment.
Build breadcrumb URLs from project.urls.production instead of localePath(...) alone.
Current item values are host-dependent and can produce duplicate SEO identities across clone domains.
Suggested fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+ const tosBreadcrumbUrl = new URL(localePath('/terms-of-service'), project.urls.production).toString()
+
useSchemaOrg(() => [
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.legal.termsOfService'), item: localePath('/terms-of-service') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.legal.termsOfService'), item: tosBreadcrumbUrl }
]
}),
defineWebPage()
])📝 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.
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.legal.termsOfService'), item: localePath('/terms-of-service') } | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const tosBreadcrumbUrl = new URL(localePath('/terms-of-service'), project.urls.production).toString() | |
| useSchemaOrg(() => [ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.legal.termsOfService'), item: tosBreadcrumbUrl } | |
| ] | |
| }), | |
| defineWebPage() | |
| ]) |
🤖 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 `@pages/terms-of-service.vue` around lines 15 - 16, The breadcrumb items
currently use localePath(...) which produces host-dependent URLs; change the
breadcrumb construction to build canonical URLs using project.urls.production as
the base (e.g., replace item: localePath('/') and item:
localePath('/terms-of-service') with items built by resolving the localePath
path against project.urls.production using a URL-safe join or the URL
constructor). Update the code that produces the breadcrumb array in
pages/terms-of-service.vue (references: localePath, t, and the breadcrumb
variable/array) to import or reference project.urls.production and use it as the
base for generating item values, ensuring proper handling of leading/trailing
slashes so the final item strings are absolute production URLs.
left a comment
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 `@components/pages/posts/post/PostMedia.vue`:
- Around line 282-295: The current race allows two concurrent initializations
because reloadVideoPlayer calls createVideoPlayer() directly; change
reloadVideoPlayer to reuse the shared init guard by calling
initializeVideoPlayer() (or by checking/awaiting videoPlayerInitPromise) instead
of calling createVideoPlayer() directly, and ensure createVideoPlayer is only
invoked from initializeVideoPlayer after setting videoPlayerInitPromise (so any
concurrent callers await the same promise); apply the same guard pattern to the
other init entrypoints referenced (the idle/focus/pointer init blocks) to
prevent double-instantiation of FluidPlayer for the same video element.
🪄 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: 7faa9e12-edfe-4d7e-b44a-56c6d4041507
📒 Files selected for processing (3)
components/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/post/PostMedia.vuetest/pages/premium/backup.test.ts
💤 Files with no reviewable changes (1)
- test/pages/premium/backup.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
components/**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Components are auto-imported without path prefix in Nuxt — use
<DomainSelector>not<Input/DomainSelector>
Files:
components/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/post/PostMedia.vue
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Images should be generated at 1x density only (webp format) via the custom imgproxy provider to reduce bandwidth
Files:
components/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/post/PostMedia.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Canonical URLs must point to production (`https://r34.app/…`) even when served from clone domains — use `project.urls.production` for canonicals
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Prefer high-impact, measurable optimizations over small rewrites — verify substantial performance changes with production builds, tests, request traces, and Lighthouse before committing
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Do not add app-level HTML compression unless a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Keep `features.inlineStyles: false` unless new measurements justify revisiting it
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Keep `formkit/auto-animate` route-scoped unless it is used broadly
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Do not add service worker logic — the PWA service worker is intentionally disabled (`selfDestroying: true`)
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Source map uploads for Sentry only happen in production Docker builds and require `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` build args
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-19T08:44:22.568Z
Learning: Prettier configuration uses 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates
🔇 Additional comments (2)
components/pages/posts/post/PostMedia.vue (1)
40-43: Verify thatpostImageSizesmatches@nuxt/image'ssizescontract.This is the same concern raised earlier:
postImageSizesis still an object and bothNuxtPicturepaths pass it straight to:sizes. If this version only accepts the documented space-separated string form, responsive sizing will silently stop working here.For `@nuxt/image` 2.0.0, what input formats are accepted for the `sizes` prop on `NuxtImg` and `NuxtPicture`? Is an object like { sm: '400px', md: '768px' } supported, or must it be a space-separated string such as "sm:400px md:768px"?Also applies to: 552-552, 579-579
components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-30
left a comment
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
plugins/035.sentry.client.ts (1)
47-53:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRemove the private
@sentry/nuxtdeep import.
@sentry/nuxt10.52.0 only exports.,./module, and./module/plugins, and Sentry's Nuxt setup docs show client initialization viaimport * as Sentry from '@sentry/nuxt'. Importing../node_modules/@sentry/nuxt/build/module/runtime/utils.jsis outside that public contract, so this can break on SDK upgrades even if it happens to work today. (raw.githubusercontent.com)🤖 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 `@plugins/035.sentry.client.ts` around lines 47 - 53, The code currently imports a private module via "../node_modules/@sentry/nuxt/build/module/runtime/utils.js" to get isNuxtError/reportNuxtError/buildSentryClientInitOptions; remove that deep import and instead rely only on the public API from '`@sentry/nuxt`' (the Sentry variable already imported) or re-implement the minimal helper logic locally; update the Promise.all to drop the risky import and then replace usages of isNuxtError/reportNuxtError/buildSentryClientInitOptions in the surrounding code to call the public Sentry APIs or your local helper functions with equivalent behavior.
🤖 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 `@plugins/035.sentry.client.ts`:
- Around line 46-84: The plugin currently swallows errors from the dynamic
imports or Sentry.init(), leaving initPromise set so subsequent interactions
won't retry; update the catch block that wraps the imports + Sentry.init() to
clear or reset the initialization sentinel (e.g., initPromise) so the
watcher/interaction trigger can attempt initialization again, and keep the error
logged; reference the init flow around Sentry.init, the dynamic imports (Sentry,
isNuxtError, reportNuxtError, buildSentryClientInitOptions) and the catch block
so you locate and reset the same variable that prevents retries.
---
Duplicate comments:
In `@plugins/035.sentry.client.ts`:
- Around line 47-53: The code currently imports a private module via
"../node_modules/@sentry/nuxt/build/module/runtime/utils.js" to get
isNuxtError/reportNuxtError/buildSentryClientInitOptions; remove that deep
import and instead rely only on the public API from '`@sentry/nuxt`' (the Sentry
variable already imported) or re-implement the minimal helper logic locally;
update the Promise.all to drop the risky import and then replace usages of
isNuxtError/reportNuxtError/buildSentryClientInitOptions in the surrounding code
to call the public Sentry APIs or your local helper functions with equivalent
behavior.
🪄 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: 556797e4-3ecb-46e3-8e1d-640837e9d6a9
📒 Files selected for processing (1)
plugins/035.sentry.client.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt 4 with Vue 3 and TypeScript for the application framework
Files:
plugins/035.sentry.client.ts
**/*.{ts,tsx,js,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:
plugins/035.sentry.client.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch, use URL.parse() when the parsed URL object is needed with URL.canParse() fallback in browser code if compatibility matters
Files:
plugins/035.sentry.client.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless replacing one has a clear, measured payoff
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: After substantial performance changes, verify with a production build, relevant tests, request traces, and Lighthouse against the built app before deciding the change is worth keeping
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Do not add app-level HTML compression unless a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Keep features.inlineStyles: false unless new measurements justify revisiting it
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Keep formkit/auto-animate route-scoped unless it is used broadly
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: The service worker is intentionally disabled (selfDestroying: true); do not add service worker logic
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: Source map uploads in Docker production builds only happen when SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN build args are provided; set SENTRY_UPLOAD_SOURCE_MAPS=false to skip them
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T07:26:14.436Z
Learning: In multi-stage Docker production build, use pnpm install --frozen-lockfile in build stage and copy only .output/ to production stage (no node_modules needed as Nitro bundles everything)
| try { | ||
| const [Sentry, { isNuxtError }, { reportNuxtError }, { buildSentryClientInitOptions }] = await Promise.all([ | ||
| import('@sentry/nuxt'), | ||
| import('nuxt/app'), | ||
| // Risky on purpose: reuse the `@sentry/nuxt/module` internal helper for Nuxt/Vue error reporting. | ||
| // This is a deep import and may break on Sentry upgrades or due to package "exports" restrictions. | ||
| import('../node_modules/@sentry/nuxt/build/module/runtime/utils.js'), | ||
| import('~/sentry.client.options') | ||
| ]) | ||
|
|
||
| Sentry.init( | ||
| buildSentryClientInitOptions({ | ||
| dsn, | ||
| Sentry | ||
| }) | ||
| ) | ||
| Sentry.init( | ||
| buildSentryClientInitOptions({ | ||
| dsn, | ||
| Sentry | ||
| }) | ||
| ) | ||
|
|
||
| // NOTE: We intentionally do NOT install `@sentry/vue`'s `vueIntegration` here. | ||
| // With interaction-gated init, the Vue app is already mounted, and `@sentry/vue` will warn: | ||
| // "Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`." | ||
| // We rely on Nuxt hooks (`app:error` + `vue:error`) + `reportNuxtError` instead. | ||
| // NOTE: We intentionally do NOT install `@sentry/vue`'s `vueIntegration` here. | ||
| // With interaction-gated init, the Vue app is already mounted, and `@sentry/vue` will warn: | ||
| // "Misconfigured SDK. Vue app is already mounted. Make sure to call `app.mount()` after `Sentry.init()`." | ||
| // We rely on Nuxt hooks (`app:error` + `vue:error`) + `reportNuxtError` instead. | ||
|
|
||
| // Capture Nuxt-level errors (after init) | ||
| nuxtApp.hook('app:error', (error) => { | ||
| if (isNuxtError(error)) { | ||
| if (error.status >= 300 && error.status < 500) { | ||
| return | ||
| // Capture Nuxt-level errors (after init) | ||
| nuxtApp.hook('app:error', (error) => { | ||
| if (isNuxtError(error)) { | ||
| if (error.status >= 300 && error.status < 500) { | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
|
||
| reportNuxtError({ error }) | ||
| }) | ||
| reportNuxtError({ error }) | ||
| }) | ||
|
|
||
| nuxtApp.hook('vue:error', (error, instance, info) => { | ||
| reportNuxtError({ error, instance, info }) | ||
| }) | ||
| nuxtApp.hook('vue:error', (error, instance, info) => { | ||
| reportNuxtError({ error, instance, info }) | ||
| }) | ||
| } catch (error) { | ||
| console.error('Failed to initialize Sentry client', error) | ||
| } |
There was a problem hiding this comment.
Don't let a transient init failure permanently disable Sentry.
If any import or Sentry.init() throws, the catch at Lines 82-84 swallows the error after initPromise has already been set. Because the watcher is stopped on Line 36 after the first interaction, that leaves this plugin in a "initialized enough to skip, not initialized enough to report" state for the rest of the session.
🤖 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 `@plugins/035.sentry.client.ts` around lines 46 - 84, The plugin currently
swallows errors from the dynamic imports or Sentry.init(), leaving initPromise
set so subsequent interactions won't retry; update the catch block that wraps
the imports + Sentry.init() to clear or reset the initialization sentinel (e.g.,
initPromise) so the watcher/interaction trigger can attempt initialization
again, and keep the error logged; reference the init flow around Sentry.init,
the dynamic imports (Sentry, isNuxtError, reportNuxtError,
buildSentryClientInitOptions) and the catch block so you locate and reset the
same variable that prevents retries.
2d2b10f to
d294007
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
plugins/050.formbricks.client.ts (1)
31-52:⚠️ Potential issue | 🟠 Major | ⚡ Quick winOne-shot deferred init has no failure recovery path.
If
import('@formbricks/js')orsetup()fails once, the watcher is already stopped and initialization never retries in-session.Suggested fix
+ let loadPromise: Promise<void> | null = null + async function loadFormbricks() { - if (formbricksInstance) { - return - } - - const { default: formbricks } = await import('`@formbricks/js`') - formbricksInstance = formbricks - - await formbricksInstance.setup({ - environmentId: formbricksEnvironmentId, - appUrl: formbricksAppUrl - }) + if (formbricksInstance) return + if (loadPromise) return loadPromise + + loadPromise = (async () => { + const { default: formbricks } = await import('`@formbricks/js`') + await formbricks.setup({ + environmentId: formbricksEnvironmentId, + appUrl: formbricksAppUrl + }) + formbricksInstance = formbricks + })().catch((error) => { + loadPromise = null + console.error('Failed to initialize Formbricks', error) + }) + + return loadPromise }🤖 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 `@plugins/050.formbricks.client.ts` around lines 31 - 52, The one-shot init currently stops the watcher even if import('`@formbricks/js`') or formbricksInstance.setup(...) fails, so add failure handling inside loadFormbricks: wrap the dynamic import and setup in try/catch, log the error, and do not let the outer watcher call stop() run on failure; instead only call stop() (or mark success) after setup completes successfully; alternatively implement a simple retry/backoff inside loadFormbricks that re-attempts import/setup a few times before giving up, referencing loadFormbricks, formbricksInstance, formbricksEnvironmentId and formbricksAppUrl so the watcher keeps running until initialization succeeds.
♻️ Duplicate comments (10)
Dockerfile (1)
10-10:⚠️ Potential issue | 🟠 Major | ⚡ Quick winChange
SENTRY_UPLOAD_SOURCE_MAPSdefault tofalseto prevent build failures without credentials.The current default of
truewill cause builds to fail when Sentry credentials (SENTRY_ORG,SENTRY_PROJECT,SENTRY_AUTH_TOKEN) are not provided, breaking non-production and local builds.🔧 Proposed fix
-ARG SENTRY_UPLOAD_SOURCE_MAPS=true +ARG SENTRY_UPLOAD_SOURCE_MAPS=falseBased on learnings: "Source map uploads only happen in production Docker builds; set SENTRY_UPLOAD_SOURCE_MAPS=false to skip if SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN not available".
🤖 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 `@Dockerfile` at line 10, Change the Dockerfile build ARG default from ARG SENTRY_UPLOAD_SOURCE_MAPS=true to ARG SENTRY_UPLOAD_SOURCE_MAPS=false so source map uploading is skipped by default; keep the variable name SENTRY_UPLOAD_SOURCE_MAPS and ensure production CI/build pipelines explicitly override it to true when SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN are provided.pages/terms-of-service.vue (1)
12-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBuild breadcrumb URLs from
project.urls.productionto ensure canonical URLs.Breadcrumb
itemvalues currently uselocalePath(...)alone, which produces host-dependent URLs that can create duplicate SEO identities across clone domains. This is the same pattern as inpages/legal.vue.🔧 Proposed fix
useSeoMeta({ title: computed(() => t('pages.legal.termsOfService')), description: computed(() => t('pages.legal.termsOfServiceDescription', { name: project.name })) }) + + const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const tosBreadcrumbUrl = new URL(localePath('/terms-of-service'), project.urls.production).toString() useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.termsOfService'), item: localePath('/terms-of-service') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.termsOfService'), item: tosBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/terms-of-service.vue` around lines 12 - 20, The breadcrumb items built in useSchemaOrg / defineBreadcrumb currently call localePath(...) directly, producing host-dependent URLs; change them to build canonical URLs using project.urls.production combined with localePath (e.g., prepend project.urls.production to the localePath result) so the item values point to the production domain; update the item entries inside defineBreadcrumb (the array with { name: t('nav.home'), item: localePath('/') } and similar) to use project.urls.production + localePath('/...') while keeping t(...) for names and leave defineWebPage unchanged.pages/premium/forgot-password.vue (1)
40-48:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb schema items.
Breadcrumb
itemvalues use host-relativelocalePath(...), so structured-data URL identity changes across clone domains. Build them fromproject.urls.production+ the localized path.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() + const forgotPasswordBreadcrumbUrl = new URL(localePath('/premium/forgot-password'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, - { name: t('pages.premium.forgotPassword.seoTitle'), item: localePath('/premium/forgot-password') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, + { name: t('pages.premium.forgotPassword.seoTitle'), item: forgotPasswordBreadcrumbUrl } ] }) ])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains."
🤖 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 `@pages/premium/forgot-password.vue` around lines 40 - 48, The breadcrumb schema currently uses host-relative localePath(...) values inside defineBreadcrumb's itemListElement, which makes URLs vary by clone domain; update each breadcrumb item's item to build a production-absolute URL by prepending project.urls.production to the localized path (e.g., replace item: localePath('/') with item: project.urls.production + localePath('/')) within the useSchemaOrg/defineBreadcrumb block so the structured-data always points to the canonical production URL.assets/js/sidebarLinks.ts (1)
33-36:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
project.urls.productionfor the FAQ URL instead of a hardcoded domain.This link still points to a fixed host, which produces non-canonical navigation on clone domains.
🔧 Proposed fix
{ nameKey: 'nav.faq', - href: 'https://rule34.app/frequently-asked-questions', + href: new URL('frequently-asked-questions', project.urls.production).toString(), isExternal: true, icon: QuestionMarkCircleIcon },As per coding guidelines: "Use project.urls.production for canonical URLs to prevent duplicate content across clone domains."
🤖 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 `@assets/js/sidebarLinks.ts` around lines 33 - 36, The FAQ link in the sidebarLinks entry (the object with nameKey 'nav.faq' and href currently set to the hardcoded 'https://rule34.app/frequently-asked-questions') should use the canonical base from project.urls.production instead of a fixed domain; update the href assignment to build the URL from project.urls.production + '/frequently-asked-questions' (ensure project.urls is imported/available in sidebarLinks.ts) so cloned domains use the correct canonical URL.pages/index.vue (1)
650-667:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for structured-data items/targets.
defineBreadcrumb.itemanddefineSearchAction.targetare still relative; schema URLs should be anchored toproject.urls.production.Suggested patch
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() +const searchTargetUrl = new URL('/posts/rule34.xxx?tags={search_term_string}', project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ { name: t('nav.home'), - item: localePath('/') + item: homeBreadcrumbUrl } ] }), @@ defineSearchAction({ - target: '/posts/rule34.xxx?tags={search_term_string}' + target: searchTargetUrl })As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/index.vue` around lines 650 - 667, The breadcrumb and search-action schema values are using relative URLs; update the structured-data to use production-absolute URLs by building them from project.urls.production: in the useSchemaOrg call, change defineBreadcrumb.itemListElement entries (the item/url produced by localePath('/')) to point at project.urls.production combined with the localized path, and change defineSearchAction({ target }) to use project.urls.production + the search path (e.g. project.urls.production + '/posts/rule34.xxx?tags={search_term_string}'), ensuring you reference defineBreadcrumb, itemListElement and defineSearchAction so schema.org outputs point to the canonical production domain.pages/premium/saved-posts/[domain].vue (1)
618-624:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute breadcrumb URLs in schema.
These
itemvalues are host-relative (localePath(...),route.path), so breadcrumb schema will vary by serving domain.Suggested patch
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() +const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() +const savedPostsBreadcrumbUrl = new URL(route.path, project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, - { name: t('pages.premium.savedPostsPage.title'), item: route.path } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, + { name: t('pages.premium.savedPostsPage.title'), item: savedPostsBreadcrumbUrl } ] }) ])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/premium/saved-posts/`[domain].vue around lines 618 - 624, The breadcrumb schema currently sets each item's "item" to host-relative values (localePath(...) and route.path) inside useSchemaOrg/defineBreadcrumb's itemListElement; replace those with production-absolute URLs by prepending the project's production base URL (project.urls.production or the runtime config canonical base) and composing full paths for localePath and route.path so the schema always uses production URLs. Update the code that builds itemListElement to call the production base + localePath(...) and production base + route.path (or use a helper that returns productionAbsolutePath) so the Schema.org breadcrumb items are domain-independent.components/pages/posts/navigation/search/TagCollections.vue (1)
17-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAwait lazy toast calls to avoid unhandled rejections.
useLazyToast()is introduced here, buttoast.error(...)call sites in this component are still fire-and-forget. If lazy import fails, this can leak unhandled promise rejections.Suggested patch
- function createTagCollectionFromSelectedTags() { + async function createTagCollectionFromSelectedTags() { @@ - toast.error(t('toasts.selectAtLeastOneTag')) + await toast.error(t('toasts.selectAtLeastOneTag')) @@ - toast.error(t('toasts.tagCollectionExists')) + await toast.error(t('toasts.tagCollectionExists'))🤖 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 `@components/pages/posts/navigation/search/TagCollections.vue` at line 17, The component currently calls toast methods returned from useLazyToast() without awaiting them, risking unhandled promise rejections if the lazy import fails; update all fire-and-forget calls to the toast instance (e.g., toast.error(...), toast.success(...)) to await the returned promise and mark the enclosing handlers as async if needed so the await is valid; locate useLazyToast() usage in TagCollections.vue and change each toast invocation to await toast.<method>(...) and ensure any functions that call them (event handlers, methods, lifecycle hooks) are declared async so the promise is properly handled.pages/premium/additional-boorus.vue (1)
181-187:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBuild breadcrumb schema URLs from
project.urls.production.Current breadcrumb
itemvalues are locale-relative and host-dependent.Suggested patch
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() +const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() +const additionalBoorusBreadcrumbUrl = new URL(localePath('/premium/additional-boorus'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, - { name: t('pages.premium.additionalBoorusPage.seoTitle'), item: localePath('/premium/additional-boorus') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, + { name: t('pages.premium.additionalBoorusPage.seoTitle'), item: additionalBoorusBreadcrumbUrl } ] }) ])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/premium/additional-boorus.vue` around lines 181 - 187, The breadcrumb schema currently passes locale-relative URLs via localePath to defineBreadcrumb (see useSchemaOrg and defineBreadcrumb with itemListElement), but needs canonical absolute URLs built from project.urls.production; update the itemListElement entries to construct full production URLs by combining project.urls.production with the localePath(...) result (e.g., new URL(localePath('/...'), project.urls.production).toString()) for each item so the schema uses project.urls.production as the host instead of locale-relative paths.components/pages/posts/post/PostMedia.vue (1)
40-43:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPass
sizesas a Nuxt Image sizes string, not an object.
postImageSizesis currently an object and is bound intoNuxtPicture :sizes. If@nuxt/imageexpects the standard sizes string format, this prevents intended responsive sizing from being applied.Proposed fix
- const postImageSizes = { - sm: '400px', - md: '768px' - } + const postImageSizes = 'sm:400px md:768px'In `@nuxt/image` v2, what type/format does the `sizes` prop accept for `NuxtPicture` and `NuxtImg`? Is an object like `{ sm: '400px', md: '768px' }` supported, or must it be a string like `sm:400px md:768px`?Also applies to: 552-552, 579-579
🤖 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 `@components/pages/posts/post/PostMedia.vue` around lines 40 - 43, postImageSizes is defined as an object but NuxtPicture/NuxtImg expect a single sizes string (e.g. viewport/alias-size pairs), so change postImageSizes from an object to a string in the Nuxt-compatible format and update any bindings that pass it into NuxtPicture/:sizes or NuxtImg/:sizes (references: postImageSizes and NuxtPicture :sizes usage). Construct the sizes string using the same breakpoints (e.g. "sm:400px md:768px" or whatever alias-to-size mapping your theme uses) and replace the object usage at the three occurrences noted so responsive sizing works.pages/premium/tag-collections.vue (1)
147-153:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse canonical absolute breadcrumb URLs in Schema.org.
itementries are still relative (localePath(...)), which can produce non-canonical structured data across clone domains. Build eachitemwithnew URL(localePath(...), project.urls.production).toString().As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/premium/tag-collections.vue` around lines 147 - 153, The breadcrumb structured data in useSchemaOrg/defineBreadcrumb currently uses relative URLs from localePath(...) which can be non-canonical; update each item in itemListElement to build an absolute canonical URL by wrapping localePath(...) with new URL(localePath(...), project.urls.production).toString() for every entry (home, premium landing, tag collections) so defineBreadcrumb receives fully qualified URLs.
🤖 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 `@layouts/default.vue`:
- Around line 30-43: The fullscreenchange handler currently bails out when
event.target is not an Element, which skips restoration; change the guard to
resolve a fallback element (use document.fullscreenElement or
document.activeElement when target is not an Element) and only return if that
resolved element is still not an Element, then continue with the existing
nextTick / setTimeout / nested requestAnimationFrame flow and call
scrollIntoView on the resolved element (reference: target,
document.fullscreenElement, document.activeElement, and the scrollIntoView call
inside the double requestAnimationFrame).
In `@pages/dmca.vue`:
- Around line 15-16: The breadcrumb schema is using host-relative URLs via
localePath(...) (e.g., the items defined with localePath('/') and
localePath('/dmca')), which must be converted to production-absolute URLs;
update the breadcrumb item values in the pages/dmca.vue schema construction to
prepend the canonical base (project.urls.production) to the localePath result
(or build the path by joining project.urls.production with the locale-aware
path) so the item fields become full production URLs instead of host-relative
ones, keeping the rest of the schema and translation calls (t(...)) unchanged.
In `@pages/legal.vue`:
- Around line 12-20: Breadcrumb items are currently built using localePath(...)
which yields host-dependent URLs; update the defineBreadcrumb itemListElement
creation inside useSchemaOrg to construct canonical URLs by prefixing localePath
routes with project.urls.production (e.g., join project.urls.production with
localePath('/...') or use a URL builder) so item values use production host;
update the code that sets item for each entry (the array in defineBreadcrumb) to
derive the full URL from project.urls.production + localePath(...) while
preserving names from t('nav.home') and t('pages.legal.title').
In `@pages/tags/`[domain]/[tag].vue:
- Around line 154-162: The breadcrumb JSON-LD currently uses localePath(...) and
route.path which produce relative/clone-host URLs; update the defineBreadcrumb
itemListElement construction to build absolute production URLs using
project.urls.production as the base (e.g., join project.urls.production with '/'
or the relevant path) for each item: replace localePath('/') and
localePath(`/posts/${selectedBooru.value.domain}`) and route.path with canonical
URLs derived from project.urls.production + the proper path segments while
keeping display names from t(...) and pageTitle.value; ensure you reference
selectedBooru.value.domain and pageTitle.value when composing those production
URLs so schema remains canonical across domains.
In `@test/server-mocks/plugin.ts`:
- Around line 28-29: Replace the defensive fallback pattern around
defineNitroPlugin (defined as defineNitroPluginSafe) with an explicit import of
defineNitroPlugin from 'nitro' and use defineNitroPlugin directly; remove the
typeof check and the defineNitroPluginSafe wrapper and update any references to
use defineNitroPlugin to match the pattern used in
server/plugins/fix-canonical-queries.ts.
---
Outside diff comments:
In `@plugins/050.formbricks.client.ts`:
- Around line 31-52: The one-shot init currently stops the watcher even if
import('`@formbricks/js`') or formbricksInstance.setup(...) fails, so add failure
handling inside loadFormbricks: wrap the dynamic import and setup in try/catch,
log the error, and do not let the outer watcher call stop() run on failure;
instead only call stop() (or mark success) after setup completes successfully;
alternatively implement a simple retry/backoff inside loadFormbricks that
re-attempts import/setup a few times before giving up, referencing
loadFormbricks, formbricksInstance, formbricksEnvironmentId and formbricksAppUrl
so the watcher keeps running until initialization succeeds.
---
Duplicate comments:
In `@assets/js/sidebarLinks.ts`:
- Around line 33-36: The FAQ link in the sidebarLinks entry (the object with
nameKey 'nav.faq' and href currently set to the hardcoded
'https://rule34.app/frequently-asked-questions') should use the canonical base
from project.urls.production instead of a fixed domain; update the href
assignment to build the URL from project.urls.production +
'/frequently-asked-questions' (ensure project.urls is imported/available in
sidebarLinks.ts) so cloned domains use the correct canonical URL.
In `@components/pages/posts/navigation/search/TagCollections.vue`:
- Line 17: The component currently calls toast methods returned from
useLazyToast() without awaiting them, risking unhandled promise rejections if
the lazy import fails; update all fire-and-forget calls to the toast instance
(e.g., toast.error(...), toast.success(...)) to await the returned promise and
mark the enclosing handlers as async if needed so the await is valid; locate
useLazyToast() usage in TagCollections.vue and change each toast invocation to
await toast.<method>(...) and ensure any functions that call them (event
handlers, methods, lifecycle hooks) are declared async so the promise is
properly handled.
In `@components/pages/posts/post/PostMedia.vue`:
- Around line 40-43: postImageSizes is defined as an object but
NuxtPicture/NuxtImg expect a single sizes string (e.g. viewport/alias-size
pairs), so change postImageSizes from an object to a string in the
Nuxt-compatible format and update any bindings that pass it into
NuxtPicture/:sizes or NuxtImg/:sizes (references: postImageSizes and NuxtPicture
:sizes usage). Construct the sizes string using the same breakpoints (e.g.
"sm:400px md:768px" or whatever alias-to-size mapping your theme uses) and
replace the object usage at the three occurrences noted so responsive sizing
works.
In `@Dockerfile`:
- Line 10: Change the Dockerfile build ARG default from ARG
SENTRY_UPLOAD_SOURCE_MAPS=true to ARG SENTRY_UPLOAD_SOURCE_MAPS=false so source
map uploading is skipped by default; keep the variable name
SENTRY_UPLOAD_SOURCE_MAPS and ensure production CI/build pipelines explicitly
override it to true when SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN are
provided.
In `@pages/index.vue`:
- Around line 650-667: The breadcrumb and search-action schema values are using
relative URLs; update the structured-data to use production-absolute URLs by
building them from project.urls.production: in the useSchemaOrg call, change
defineBreadcrumb.itemListElement entries (the item/url produced by
localePath('/')) to point at project.urls.production combined with the localized
path, and change defineSearchAction({ target }) to use project.urls.production +
the search path (e.g. project.urls.production +
'/posts/rule34.xxx?tags={search_term_string}'), ensuring you reference
defineBreadcrumb, itemListElement and defineSearchAction so schema.org outputs
point to the canonical production domain.
In `@pages/premium/additional-boorus.vue`:
- Around line 181-187: The breadcrumb schema currently passes locale-relative
URLs via localePath to defineBreadcrumb (see useSchemaOrg and defineBreadcrumb
with itemListElement), but needs canonical absolute URLs built from
project.urls.production; update the itemListElement entries to construct full
production URLs by combining project.urls.production with the localePath(...)
result (e.g., new URL(localePath('/...'), project.urls.production).toString())
for each item so the schema uses project.urls.production as the host instead of
locale-relative paths.
In `@pages/premium/forgot-password.vue`:
- Around line 40-48: The breadcrumb schema currently uses host-relative
localePath(...) values inside defineBreadcrumb's itemListElement, which makes
URLs vary by clone domain; update each breadcrumb item's item to build a
production-absolute URL by prepending project.urls.production to the localized
path (e.g., replace item: localePath('/') with item: project.urls.production +
localePath('/')) within the useSchemaOrg/defineBreadcrumb block so the
structured-data always points to the canonical production URL.
In `@pages/premium/saved-posts/`[domain].vue:
- Around line 618-624: The breadcrumb schema currently sets each item's "item"
to host-relative values (localePath(...) and route.path) inside
useSchemaOrg/defineBreadcrumb's itemListElement; replace those with
production-absolute URLs by prepending the project's production base URL
(project.urls.production or the runtime config canonical base) and composing
full paths for localePath and route.path so the schema always uses production
URLs. Update the code that builds itemListElement to call the production base +
localePath(...) and production base + route.path (or use a helper that returns
productionAbsolutePath) so the Schema.org breadcrumb items are
domain-independent.
In `@pages/premium/tag-collections.vue`:
- Around line 147-153: The breadcrumb structured data in
useSchemaOrg/defineBreadcrumb currently uses relative URLs from localePath(...)
which can be non-canonical; update each item in itemListElement to build an
absolute canonical URL by wrapping localePath(...) with new URL(localePath(...),
project.urls.production).toString() for every entry (home, premium landing, tag
collections) so defineBreadcrumb receives fully qualified URLs.
In `@pages/terms-of-service.vue`:
- Around line 12-20: The breadcrumb items built in useSchemaOrg /
defineBreadcrumb currently call localePath(...) directly, producing
host-dependent URLs; change them to build canonical URLs using
project.urls.production combined with localePath (e.g., prepend
project.urls.production to the localePath result) so the item values point to
the production domain; update the item entries inside defineBreadcrumb (the
array with { name: t('nav.home'), item: localePath('/') } and similar) to use
project.urls.production + localePath('/...') while keeping t(...) for names and
leave defineWebPage 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: b7cc8456-121a-465a-a22b-9523367a3265
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (78)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/pt.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/server/path.test.ts
💤 Files with no reviewable changes (4)
- app.vue
- server/plugins/lcp-preload-fetchpriority.ts
- app/router.options.ts
- test/pages/premium/backup.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,vue,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt 4 (SSR with Nitro server) with Vue 3 and TypeScript
Files:
composables/navigation/useMenu.tscomponents/pages/home/Newsletter.vuecomposables/useIdleTask.tscomponents/layout/ClientToaster.vuecomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuetest/server/path.test.tscomponents/pages/home/PageHistory.vueassets/js/nuxt-image/imgproxy.provider.tspages/legal.vuepages/terms-of-service.vuetest/server-mocks/plugin.tspages/premium/forgot-password.vueassets/js/sidebarLinks.tstest/helper.tscomponents/shared/ContentContainer.vueconfig/i18n.tsassets/js/RouterHelper.tspages/premium/sign-in.vueplugins/040.matomo.client.tscomposables/useInteractionDetector.tscomponents/pages/posts/post/PostTag.vuepages/settings.vueserver/api/_sitemap-urls.tscomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuecomposables/useFaviconUrl.tspages/privacy-policy.vuepages/premium/index.vuecomponents/layout/DialogManager.vuecomposables/useLazyToast.tscomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuecomposables/useDialogManagerState.tspages/premium/dashboard.vueplugins/035.sentry.client.tspages/tags/[domain]/[tag].vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vueplugins/050.formbricks.client.tscomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/saved-posts/[domain].vuetest/assets/router-helper.test.tscomponents/layout/navigation/SidebarWrapper.vuepages/premium/additional-boorus.vuecomponents/layout/navigation/Navbar.vuepages/index.vuetest/server/fix-canonical-queries.test.tscomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuenuxt.config.jscomponents/pages/posts/post/PostMedia.vuetest/pages/posts.test.tspages/posts/[domain].vue
**/*.{ts,tsx,vue,js,css,json}
📄 CodeRabbit inference engine (AGENTS.md)
Use Prettier for code formatting with 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates
Files:
composables/navigation/useMenu.tscomponents/pages/home/Newsletter.vuecomposables/useIdleTask.tscomponents/layout/ClientToaster.vuei18n/locales/ru.jsoncomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuetest/server/path.test.tscomponents/pages/home/PageHistory.vueassets/js/nuxt-image/imgproxy.provider.tspages/legal.vuepages/terms-of-service.vuetest/server-mocks/plugin.tsi18n/locales/ja.jsonpages/premium/forgot-password.vueassets/js/sidebarLinks.tstest/helper.tscomponents/shared/ContentContainer.vueconfig/i18n.tsassets/js/RouterHelper.tspages/premium/sign-in.vueplugins/040.matomo.client.tsi18n/locales/en.jsoncomposables/useInteractionDetector.tscomponents/pages/posts/post/PostTag.vuepages/settings.vueserver/api/_sitemap-urls.tscomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuei18n/locales/fr.jsonpages/dmca.vuecomponents/pages/home/PageHistorySection.vuecomposables/useFaviconUrl.tspages/privacy-policy.vuei18n/locales/es.jsonpages/premium/index.vuecomponents/layout/DialogManager.vuecomposables/useLazyToast.tsi18n/locales/pt.jsoncomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuecomposables/useDialogManagerState.tspages/premium/dashboard.vuei18n/locales/de.jsonplugins/035.sentry.client.tspages/tags/[domain]/[tag].vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vueplugins/050.formbricks.client.tscomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/saved-posts/[domain].vuetest/assets/router-helper.test.tscomponents/layout/navigation/SidebarWrapper.vuepages/premium/additional-boorus.vuecomponents/layout/navigation/Navbar.vuepages/index.vuetest/server/fix-canonical-queries.test.tscomponents/pages/home/FeaturedTags.vuepackage.jsoncomponents/pages/posts/post/PostComponent.vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuenuxt.config.jscomponents/pages/posts/post/PostMedia.vuetest/pages/posts.test.tspages/posts/[domain].vue
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when parsed object is needed
Files:
composables/navigation/useMenu.tscomposables/useIdleTask.tstest/server/path.test.tsassets/js/nuxt-image/imgproxy.provider.tstest/server-mocks/plugin.tsassets/js/sidebarLinks.tstest/helper.tsconfig/i18n.tsassets/js/RouterHelper.tsplugins/040.matomo.client.tscomposables/useInteractionDetector.tsserver/api/_sitemap-urls.tscomposables/useFaviconUrl.tscomposables/useLazyToast.tscomposables/useDialogManagerState.tsplugins/035.sentry.client.tsplugins/050.formbricks.client.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tsnuxt.config.jstest/pages/posts.test.ts
**/*.{css,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use TailwindCSS v4 via
@tailwindcss/viteVite plugin (NOT PostCSS)
Files:
components/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vuecomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuecomponents/pages/home/PageHistory.vuepages/legal.vuepages/terms-of-service.vuepages/premium/forgot-password.vuecomponents/shared/ContentContainer.vuepages/premium/sign-in.vuecomponents/pages/posts/post/PostTag.vuepages/settings.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuepages/premium/index.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuepages/premium/dashboard.vuepages/tags/[domain]/[tag].vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/saved-posts/[domain].vuecomponents/layout/navigation/SidebarWrapper.vuepages/premium/additional-boorus.vuecomponents/layout/navigation/Navbar.vuepages/index.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Register Vue components without path prefix; import as not <Folder/ComponentName>
Validate image delivery in environments where imgproxy can resolve source URLs; non-premium SPA navigations use direct image path
Files:
components/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/home/PageHistory.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/Sidebar.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use project.urls.production for canonical URLs to point to production even when served from clone domains
Generate images at 1x density only in webp format to reduce bandwidth
Use
@nuxt/imagev2 module API for image preload priority via preload: { fetchPriority: 'high' } instead of patching Nitro HTML
Files:
components/pages/home/Newsletter.vuecomponents/layout/ClientToaster.vuecomponents/layout/modal/ReviewPrompt.vuepages/other-sites.vuecomponents/pages/home/PageHistory.vuepages/legal.vuepages/terms-of-service.vuepages/premium/forgot-password.vuecomponents/shared/ContentContainer.vuepages/premium/sign-in.vuecomponents/pages/posts/post/PostTag.vuepages/settings.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuepages/premium/index.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/Sidebar.vuepages/cookie-policy.vuepages/premium/dashboard.vuepages/tags/[domain]/[tag].vuecomponents/shared/ShareButton.vuelayouts/default.vuepages/premium/backup.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/saved-posts/[domain].vuecomponents/layout/navigation/SidebarWrapper.vuepages/premium/additional-boorus.vuecomponents/layout/navigation/Navbar.vuepages/index.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use useSeoMeta in page components for page-specific tags (title, description)
Use flat bracket keys for query filters (filter[sort], filter[rating]) with Vue Router's default query handling; do not use qs library unless URL contract changes
Keep
@formkit/auto-animateroute-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages
Files:
pages/other-sites.vuepages/legal.vuepages/terms-of-service.vuepages/premium/forgot-password.vuepages/premium/sign-in.vuepages/settings.vuepages/dmca.vuepages/privacy-policy.vuepages/premium/index.vuepages/cookie-policy.vuepages/premium/dashboard.vuepages/tags/[domain]/[tag].vuepages/premium/backup.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/index.vuepages/premium/tag-collections.vuepages/posts/[domain].vue
test/**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest +
@nuxt/test-utilswith Playwright browser mode for testingUse
@nuxt/test-utilswith Playwright inside describe blocks calling await setup({ browser: true })For test debug mode, import debugBrowserOptions from test/helper.ts for headful playback with slowMo
Files:
test/server/path.test.tstest/server-mocks/plugin.tstest/helper.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
test/server-mocks/plugin.ts
📄 CodeRabbit inference engine (AGENTS.md)
Mock server-side API calls via test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.js $test.nitro.plugins
Files:
test/server-mocks/plugin.ts
config/i18n.ts
📄 CodeRabbit inference engine (AGENTS.md)
Define locales in config/i18n.ts as the single source of truth, not in individual files
Files:
config/i18n.ts
package.json
📄 CodeRabbit inference engine (AGENTS.md)
Node ≥ 24 required as specified in package.json engines field
pnpm ≥ 11.1 required as pinned in packageManager field
Files:
package.json
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless clear measured payoff exists
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: After substantial performance changes, verify with production build, relevant tests, request traces, and Lighthouse before deciding to keep
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Production is behind Cloudflare with Brotli compression; do not add app-level HTML compression unless direct-origin deployment needs it with verified headers and metrics
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Keep global TanStack Vue Query plugin unless larger measured payoff appears for route-scoped QueryClient
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Source map uploads only happen in production Docker builds; set SENTRY_UPLOAD_SOURCE_MAPS=false to skip if SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN not available
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Docker multi-stage production build: use pnpm install --frozen-lockfile in build stage; production stage copies only .output/ without node_modules
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T08:30:35.432Z
Learning: Use NITRO_PRESET build arg to select deployment target in Docker production build
🔇 Additional comments (81)
pages/other-sites.vue (1)
24-31: Use production absolute URLs for breadcrumbitemvalues.At Line 27 and Line 28,
localePath(...)produces host-relative URLs, so schema breadcrumbs can vary by serving domain. Build each breadcrumb URL withproject.urls.production+localePath(...)to keep canonical structured data stable.As per coding guidelines: "
**/*.vue: Use project.urls.production for canonical URLs to point to production even when served from clone domains".composables/navigation/useMenu.ts (1)
1-1: LGTM!pnpm-workspace.yaml (1)
1-6: LGTM!components/pages/home/Newsletter.vue (1)
45-45: LGTM!composables/useIdleTask.ts (1)
1-16: LGTM!components/layout/ClientToaster.vue (1)
1-13: LGTM!i18n/locales/ru.json (1)
389-390: LGTM!Also applies to: 404-404
components/layout/modal/ReviewPrompt.vue (1)
47-49: LGTM!test/server/path.test.ts (1)
4-4: LGTM!components/pages/home/PageHistory.vue (1)
3-3: LGTM!Also applies to: 35-35
assets/js/nuxt-image/imgproxy.provider.ts (1)
41-41: LGTM!Also applies to: 79-86
.vscode/settings.json (1)
1-3: LGTM!Dockerfile (1)
20-26: LGTM!public/llms.txt (1)
1-28: LGTM!i18n/locales/ja.json (1)
389-390: LGTM!Also applies to: 404-404
pages/premium/forgot-password.vue (1)
5-5: LGTM!test/helper.ts (1)
4-7: LGTM!Also applies to: 9-17, 62-69
components/shared/ContentContainer.vue (1)
2-2: LGTM!Also applies to: 48-54
README.md (1)
103-103: LGTM!Also applies to: 106-106, 109-109, 112-112
config/i18n.ts (1)
9-12: LGTM!pages/premium/sign-in.vue (2)
61-69: Breadcrumb schema URLs are still relative (already reported).Line 64 to Line 66 still uses
localePath(...)directly foritem, which keeps structured data host-dependent on clone domains.As per coding guidelines: Use
project.urls.productionfor canonical URLs to point to production even when served from clone domains.
8-8: LGTM!pages/settings.vue (2)
32-39: Schema breadcrumbitemURLs are still relative (already reported).Line 35 and Line 36 still use host-relative
localePath(...)values, so structured data remains domain-dependent.As per coding guidelines: Use
project.urls.productionfor canonical URLs to point to production even when served from clone domains.
9-10: LGTM!assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-33, 38-46, 62-70
plugins/040.matomo.client.ts (1)
2-2: LGTM!Also applies to: 41-41, 47-47
i18n/locales/en.json (1)
389-390: LGTM!Also applies to: 404-404
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
components/pages/posts/post/PostTag.vue (1)
2-15: LGTM!Also applies to: 31-31
server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 17-17, 22-22
pages/privacy-policy.vue (1)
15-16: Previously flagged breadcrumb canonical issue is still present.Line 15 and Line 16 still use
localePath(...)directly for schema breadcrumbitemURLs; these should be production-absolute.components/pages/posts/PostsPageFooter.vue (1)
2-6: LGTM!Also applies to: 8-8, 125-125
components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-27, 29-30
i18n/locales/fr.json (1)
1-539: LGTM!pages/dmca.vue (1)
49-51: LGTM!components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!composables/useFaviconUrl.ts (1)
1-7: LGTM!i18n/locales/es.json (1)
389-390: LGTM!Also applies to: 404-404
pages/premium/index.vue (2)
202-206: Use production-absolute URLs for breadcrumb schema items.
itemListElement[*].itemis still built fromlocalePath(...), so structured data remains host-dependent on clone domains.As per coding guidelines: "For canonical URLs, always point to production (
https://r34.app/…) even when served from clone domains, usingproject.urls.production."
177-181: LGTM!Also applies to: 197-199, 666-669
pages/cookie-policy.vue (1)
12-17: Build breadcrumb schema items with production-absolute URLs.The breadcrumb
itemfields still uselocalePath(...), which keeps schema URLs host-dependent.As per coding guidelines: "For canonical URLs, always point to production (
https://r34.app/…) even when served from clone domains, usingproject.urls.production."pages/premium/dashboard.vue (2)
129-135: Use production-absolute breadcrumb URLs in structured data.
itemListElement[*].itemis still derived fromlocalePath(...), which makes schema identity host-dependent.As per coding guidelines: "For canonical URLs, always point to production (
https://r34.app/…) even when served from clone domains, usingproject.urls.production."
19-19: LGTM!Also applies to: 219-221, 262-264, 281-283
components/layout/DialogManager.vue (1)
2-77: LGTM!Also applies to: 88-88, 100-100, 116-116, 120-120
composables/useLazyToast.ts (1)
1-50: LGTM!i18n/locales/pt.json (1)
1-539: LGTM!components/layout/navigation/Sidebar.vue (1)
2-17: LGTM!Also applies to: 51-51
composables/useDialogManagerState.ts (1)
1-65: LGTM!plugins/035.sentry.client.ts (2)
47-53: Private Sentry deep import is still present.This remains an unresolved prior finding: Line 52 imports
@sentry/nuxtinternals via anode_modulesdeep path, which is upgrade-fragile and outside public API.In `@sentry/nuxt` v10.x, is importing build/module/runtime/utils.js documented as a supported public API?
45-84: Failed init still leaves Sentry permanently disabled for the session.This is also still unresolved from earlier review: if init throws,
initPromiseremains set and the interaction watcher is already stopped, so no retry path remains.pages/premium/backup.vue (1)
71-79: Breadcrumb schema URLs still depend on serving host.This repeats an existing unresolved finding on this file: Line 74-76 should use production absolute URLs rather than
localePath(...).i18n/locales/de.json (1)
1-540: LGTM!AGENTS.md (1)
1-160: LGTM!components/shared/ShareButton.vue (1)
12-43: LGTM!test/assets/router-helper.test.ts (1)
1-2: LGTM!Also applies to: 24-67
components/layout/navigation/SidebarWrapper.vue (1)
3-13: LGTM!Also applies to: 17-109
components/layout/navigation/Navbar.vue (1)
2-30: LGTM!test/server/fix-canonical-queries.test.ts (1)
14-14: LGTM!Also applies to: 24-24, 33-33, 36-36, 57-57
components/pages/home/FeaturedTags.vue (1)
4-49: LGTM!Also applies to: 73-74, 106-112
package.json (1)
5-10: LGTM!components/pages/posts/post/PostComponent.vue (1)
22-39: LGTM!Also applies to: 76-82, 98-98, 182-183, 187-347
components/pages/posts/navigation/DomainSelector.vue (1)
23-23: LGTM!Also applies to: 89-89, 91-92, 143-143, 145-146
components/pages/posts/post/PostSource.vue (1)
13-13: LGTM!Also applies to: 78-82, 164-164, 173-173, 207-207, 232-232
nuxt.config.js (1)
17-45: LGTM!Also applies to: 100-100, 108-108, 141-182, 190-195, 210-210, 283-283, 401-401, 463-463
test/pages/posts.test.ts (8)
6-57: LGTM!
69-129: LGTM!
131-183: LGTM!
185-232: LGTM!
234-336: LGTM!
338-376: LGTM!
378-464: LGTM!
466-508: LGTM!pages/posts/[domain].vue (9)
9-14: LGTM!Also applies to: 25-38
42-47: LGTM!
74-77: LGTM!
145-153: LGTM!
627-644: LGTM!
731-812: LGTM!
820-834: LGTM!
836-859: LGTM!
987-996: LGTM!Also applies to: 1167-1167
| const target = event.target | ||
|
|
||
| if (!(target instanceof Element)) { | ||
| return | ||
| } | ||
|
|
||
| await nextTick() | ||
|
|
||
| await new Promise((resolve) => setTimeout(resolve, 50)) | ||
|
|
||
| requestAnimationFrame(() => { | ||
| requestAnimationFrame(() => { | ||
| event.target.scrollIntoView({ | ||
| target.scrollIntoView({ | ||
| block: 'center', |
There was a problem hiding this comment.
Fullscreen restore target detection is too strict and likely skips restoration.
At Line 30-34, event.target is often document for fullscreenchange; the Element guard causes early return, so Line 42 never runs.
Suggested fix
- const target = event.target
-
- if (!(target instanceof Element)) {
+ const target = document.fullscreenElement ?? (event.target instanceof Element ? event.target : null)
+ if (!target) {
return
}📝 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.
| const target = event.target | |
| if (!(target instanceof Element)) { | |
| return | |
| } | |
| await nextTick() | |
| await new Promise((resolve) => setTimeout(resolve, 50)) | |
| requestAnimationFrame(() => { | |
| requestAnimationFrame(() => { | |
| event.target.scrollIntoView({ | |
| target.scrollIntoView({ | |
| block: 'center', | |
| const target = document.fullscreenElement ?? (event.target instanceof Element ? event.target : null) | |
| if (!target) { | |
| return | |
| } | |
| await nextTick() | |
| await new Promise((resolve) => setTimeout(resolve, 50)) | |
| requestAnimationFrame(() => { | |
| requestAnimationFrame(() => { | |
| target.scrollIntoView({ | |
| block: 'center', |
🤖 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 `@layouts/default.vue` around lines 30 - 43, The fullscreenchange handler
currently bails out when event.target is not an Element, which skips
restoration; change the guard to resolve a fallback element (use
document.fullscreenElement or document.activeElement when target is not an
Element) and only return if that resolved element is still not an Element, then
continue with the existing nextTick / setTimeout / nested requestAnimationFrame
flow and call scrollIntoView on the resolved element (reference: target,
document.fullscreenElement, document.activeElement, and the scrollIntoView call
inside the double requestAnimationFrame).
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.legal.dmca'), item: localePath('/dmca') } |
There was a problem hiding this comment.
Use production-absolute breadcrumb URLs in schema.
Line 15 and Line 16 currently emit host-relative breadcrumb item URLs via localePath(...). For structured data, keep them production-absolute to avoid clone-domain canonicals.
🔧 Proposed fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+const dmcaBreadcrumbUrl = new URL(localePath('/dmca'), project.urls.production).toString()
+
useSchemaOrg(() => [
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.legal.dmca'), item: localePath('/dmca') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.legal.dmca'), item: dmcaBreadcrumbUrl }
]
}),
defineWebPage()
])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
🤖 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 `@pages/dmca.vue` around lines 15 - 16, The breadcrumb schema is using
host-relative URLs via localePath(...) (e.g., the items defined with
localePath('/') and localePath('/dmca')), which must be converted to
production-absolute URLs; update the breadcrumb item values in the
pages/dmca.vue schema construction to prepend the canonical base
(project.urls.production) to the localePath result (or build the path by joining
project.urls.production with the locale-aware path) so the item fields become
full production URLs instead of host-relative ones, keeping the rest of the
schema and translation calls (t(...)) unchanged.
| useSchemaOrg(() => [ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.legal.title'), item: localePath('/legal') } | ||
| ] | ||
| }), | ||
| defineWebPage() | ||
| ]) |
There was a problem hiding this comment.
Build breadcrumb URLs from project.urls.production to ensure canonical URLs.
Breadcrumb item values currently use localePath(...) alone, which produces host-dependent URLs that can create duplicate SEO identities across clone domains.
🔧 Proposed fix
useSeoMeta({
title: () => t('pages.legal.title'),
description: () => t('pages.legal.description', { name: project.name })
})
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString()
+ const legalBreadcrumbUrl = new URL(localePath('/legal'), project.urls.production).toString()
+
useSchemaOrg(() => [
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
- { name: t('pages.legal.title'), item: localePath('/legal') }
+ { name: t('nav.home'), item: homeBreadcrumbUrl },
+ { name: t('pages.legal.title'), item: legalBreadcrumbUrl }
]
}),
defineWebPage()
])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains".
📝 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.
| useSchemaOrg(() => [ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: localePath('/') }, | |
| { name: t('pages.legal.title'), item: localePath('/legal') } | |
| ] | |
| }), | |
| defineWebPage() | |
| ]) | |
| const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() | |
| const legalBreadcrumbUrl = new URL(localePath('/legal'), project.urls.production).toString() | |
| useSchemaOrg(() => [ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { name: t('nav.home'), item: homeBreadcrumbUrl }, | |
| { name: t('pages.legal.title'), item: legalBreadcrumbUrl } | |
| ] | |
| }), | |
| defineWebPage() | |
| ]) |
🤖 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 `@pages/legal.vue` around lines 12 - 20, Breadcrumb items are currently built
using localePath(...) which yields host-dependent URLs; update the
defineBreadcrumb itemListElement creation inside useSchemaOrg to construct
canonical URLs by prefixing localePath routes with project.urls.production
(e.g., join project.urls.production with localePath('/...') or use a URL
builder) so item values use production host; update the code that sets item for
each entry (the array in defineBreadcrumb) to derive the full URL from
project.urls.production + localePath(...) while preserving names from
t('nav.home') and t('pages.legal.title').
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { | ||
| name: t('seo.postsFrom', { domain: selectedBooru.value.domain }), | ||
| item: localePath(`/posts/${selectedBooru.value.domain}`) | ||
| }, | ||
| { name: pageTitle.value, item: route.path } | ||
| ] |
There was a problem hiding this comment.
Use production absolute URLs in breadcrumb schema items.
Line 156-162 currently emits locale/relative URLs (localePath(...), route.path) in JSON-LD breadcrumbs. Build these items from project.urls.production so schema URLs stay canonical across domains.
Suggested fix
+const homeBreadcrumbUrl = computed(() => new URL(localePath('/'), project.urls.production).toString())
+const booruBreadcrumbUrl = computed(() =>
+ new URL(localePath(`/posts/${selectedBooru.value.domain}`), project.urls.production).toString()
+)
+const tagBreadcrumbUrl = computed(() => new URL(localePath(route.path), project.urls.production).toString())
useSchemaOrg(() => [
defineBreadcrumb({
itemListElement: [
- { name: t('nav.home'), item: localePath('/') },
+ { name: t('nav.home'), item: homeBreadcrumbUrl.value },
{
name: t('seo.postsFrom', { domain: selectedBooru.value.domain }),
- item: localePath(`/posts/${selectedBooru.value.domain}`)
+ item: booruBreadcrumbUrl.value
},
- { name: pageTitle.value, item: route.path }
+ { name: pageTitle.value, item: tagBreadcrumbUrl.value }
]
}),As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains."
🤖 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 `@pages/tags/`[domain]/[tag].vue around lines 154 - 162, The breadcrumb JSON-LD
currently uses localePath(...) and route.path which produce relative/clone-host
URLs; update the defineBreadcrumb itemListElement construction to build absolute
production URLs using project.urls.production as the base (e.g., join
project.urls.production with '/' or the relevant path) for each item: replace
localePath('/') and localePath(`/posts/${selectedBooru.value.domain}`) and
route.path with canonical URLs derived from project.urls.production + the proper
path segments while keeping display names from t(...) and pageTitle.value;
ensure you reference selectedBooru.value.domain and pageTitle.value when
composing those production URLs so schema remains canonical across domains.
| const defineNitroPluginSafe = | ||
| typeof defineNitroPlugin === 'function' ? defineNitroPlugin : <T extends (...args: any[]) => any>(plugin: T) => plugin |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if other plugins use defineNitroPlugin or similar defensive patterns
rg -n 'defineNitroPlugin' --type=ts -C3Repository: Rule-34/App
Length of output: 1601
🏁 Script executed:
# Check how the test plugin is injected in nuxt.config
fd -i "nuxt.config" --type f | head -5Repository: Rule-34/App
Length of output: 70
🏁 Script executed:
# View the full test/server-mocks/plugin.ts to see if there are imports
head -30 test/server-mocks/plugin.tsRepository: Rule-34/App
Length of output: 993
🏁 Script executed:
# Check for similar test plugin patterns in other Nuxt/Nitro projects or documentation
rg -n 'defineNitroPlugin|nitro.plugins' --type=ts --type=js -B2 -A2 | head -50Repository: Rule-34/App
Length of output: 1603
🏁 Script executed:
cat nuxt.config.js | grep -A 10 -B 10 'nitro.plugins\|server-mocks'Repository: Rule-34/App
Length of output: 283
🏁 Script executed:
# Check if defineNitroPlugin is imported from any package
rg -n "from\s+['\"]nitro" test/server-mocks/plugin.tsRepository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there are any other nitro plugins in the config or codebase that might show the pattern
cat nuxt.config.js | head -100Repository: Rule-34/App
Length of output: 3098
🏁 Script executed:
# Check Nitro's auto-imports by looking at the generated .nuxt directory
find .nuxt -name "*.ts" -o -name "*.js" 2>/dev/null | head -5Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there's a .nuxtrc or tsconfig.json that might affect auto-imports
cat tsconfig.json | grep -A 20 "compilerOptions\|types"Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if defineNitroPlugin needs explicit import in the test plugin
# Look at how it's being used in the production plugin
cat server/plugins/fix-canonical-queries.ts | head -5Repository: Rule-34/App
Length of output: 223
🏁 Script executed:
# Search for defineNitroPlugin imports in the codebase
rg -n "import.*defineNitroPlugin" --type=tsRepository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there's a .nuxt generated types file that shows what's auto-imported
find . -path "./.nuxt" -name "*.ts" -exec grep -l "defineNitroPlugin" {} \; 2>/dev/null | head -3Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check what's exported from the nitro package to understand where defineNitroPlugin comes from
rg -n "defineNitroPlugin" --type=ts server/plugins/ -B1 -A1Repository: Rule-34/App
Length of output: 302
🏁 Script executed:
# Look for any .d.ts files or type definitions that might show auto-imports
find . -name "*.d.ts" -exec grep -l "defineNitroPlugin" {} \; 2>/dev/null | head -3Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there's explicit guidance in Nitro documentation or comments
rg -n "auto.import|auto-import" --type=ts --type=js | head -10Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there's a way to verify if the plugin actually works without the fallback
# Look for test files that might validate the mock plugin
find test -name "*.spec.ts" -o -name "*.test.ts" | head -5Repository: Rule-34/App
Length of output: 198
🏁 Script executed:
# Let's check the actual Nuxt/Nitro version to understand auto-import behavior
grep -i "nuxt\|nitro" package.json | head -10Repository: Rule-34/App
Length of output: 361
🏁 Script executed:
# Verify the production plugin is definitely using defineNitroPlugin without import
cat server/plugins/fix-canonical-queries.ts | head -10Repository: Rule-34/App
Length of output: 488
🏁 Script executed:
# Let's check if one of the test files actually uses the mock server
head -50 test/pages/posts.test.tsRepository: Rule-34/App
Length of output: 1784
🏁 Script executed:
# Check the full nuxt.config to see the $test configuration mentioned in plugin comment
grep -A 20 -B 5 "test" nuxt.config.js | head -50Repository: Rule-34/App
Length of output: 430
🏁 Script executed:
# Verify if the plugin actually exports correctly by checking the export statement
grep -A 5 "export default defineNitroPluginSafe" test/server-mocks/plugin.tsRepository: Rule-34/App
Length of output: 231
🏁 Script executed:
# Let's verify if the mock plugin is actually being used by checking test execution
grep -r "getPostImageSrc\|booru" test/pages/posts.test.ts | head -10Repository: Rule-34/App
Length of output: 853
🏁 Script executed:
# Check if Nitro auto-imports are configured in a .nuxtrc or similar
cat .nuxtrc 2>/dev/null || cat .nuxt/nuxt.d.ts 2>/dev/null | grep -i "auto" | head -10Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Confirm what defineNitroPlugin is and where it comes from
npm list nitro 2>/dev/null | head -5Repository: Rule-34/App
Length of output: 105
🏁 Script executed:
# Check if there's a way defineNitroPlugin could be auto-imported at build time
rg -n "nitro" nuxt.config.js | grep -v "runtimeConfig"Repository: Rule-34/App
Length of output: 87
🏁 Script executed:
# Let's check if defineNitroPlugin can be imported from 'nitro' package
grep -r "from.*['\"]nitro['\"]" . --type=ts --type=js | head -10Repository: Rule-34/App
Length of output: 176
🏁 Script executed:
# Verify if the current pattern actually works by checking if the plugin is being called
# Look for any error handling or verification in the tests
grep -r "defineNitroPluginSafe\|/booru/" test/ --type=ts | head -15Repository: Rule-34/App
Length of output: 176
🏁 Script executed:
# Check Nitro version and documentation hints
cat package-lock.json 2>/dev/null | grep -A 5 '"nitro"' | head -15Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Use rg instead to check imports from nitro
rg "from.*['\"]nitro['\"]" --type ts --type jsRepository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if the tests pass with the current defineNitroPluginSafe pattern
rg "/booru/" test/ -t ts -A 2 -B 2Repository: Rule-34/App
Length of output: 5825
🏁 Script executed:
# Look at the h3 imports to see what's being imported and what's auto-imported
head -15 test/server-mocks/plugin.tsRepository: Rule-34/App
Length of output: 549
🏁 Script executed:
# Check if there's any Nitro config or auto-import setup in package.json
cat package.json | grep -A 5 -B 5 "auto"Repository: Rule-34/App
Length of output: 385
🏁 Script executed:
# Let's verify if defineNitroPlugin is available as a global or needs to be imported
rg "defineNitroPlugin" --type ts --type js -B 5 | head -40Repository: Rule-34/App
Length of output: 1187
🏁 Script executed:
# Check if there's explicit documentation or comments about defineNitroPlugin
find . -name "*.ts" -o -name "*.js" | xargs grep -l "defineNitroPlugin" | xargs grep -B 3 "defineNitroPlugin" | head -50Repository: Rule-34/App
Length of output: 894
🏁 Script executed:
# Test if the current pattern actually works by checking the test results
cat test/server/booru-mocks.test.ts | head -20Repository: Rule-34/App
Length of output: 934
🏁 Script executed:
# Final verification: check if the test plugin actually registers with Nitro
# by looking at what the fallback returns and how Nitro handles it
cat test/server-mocks/plugin.ts | sed -n '28,115p'Repository: Rule-34/App
Length of output: 2132
🏁 Script executed:
# Verify the fallback identity function signature matches what Nitro expects
rg "defineNitroPlugin\|export default" test/server-mocks/plugin.ts -A 2Repository: Rule-34/App
Length of output: 37
🏁 Script executed:
# Check if there's any evidence that defineNitroPlugin is auto-imported at build time
# by looking at how Nitro processes plugin files
rg "auto.*import|import.*auto" . --type ts --type js | head -10Repository: Rule-34/App
Length of output: 323
🏁 Script executed:
# Verify the test actually successfully uses the mock endpoint
cat test/server/booru-mocks.test.tsRepository: Rule-34/App
Length of output: 941
Import defineNitroPlugin explicitly for clarity.
The tests pass successfully and the mock endpoints work correctly, but the defensive fallback pattern here is unnecessary. Since defineNitroPlugin is auto-imported by Nuxt/Nitro, you can simplify this to an explicit import and remove the type check:
import { defineNitroPlugin } from 'nitro'Then use defineNitroPlugin directly without the fallback wrapper, matching the pattern in server/plugins/fix-canonical-queries.ts.
🤖 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-mocks/plugin.ts` around lines 28 - 29, Replace the defensive
fallback pattern around defineNitroPlugin (defined as defineNitroPluginSafe)
with an explicit import of defineNitroPlugin from 'nitro' and use
defineNitroPlugin directly; remove the typeof check and the
defineNitroPluginSafe wrapper and update any references to use defineNitroPlugin
to match the pattern used in server/plugins/fix-canonical-queries.ts.
d294007 to
ab64628
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
components/pages/home/FeaturedTags.vue (1)
106-116:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
provider="imgproxy"to optimize featured tag images.Images should use the registered
imgproxyprovider for optimization (webp, 1x density). This component is inconsistent withPostMedia.vueand other pages in the codebase that explicitly set this attribute.🔧 Proposed fix
<NuxtImg :alt="t('common.featuredTag', { name: tag.name })" :decoding="isPriorityMedia(index) ? undefined : 'async'" :fetchpriority="isPriorityMedia(index) ? undefined : 'low'" :loading="isPriorityMedia(index) ? 'eager' : 'lazy'" :preload="isPriorityMedia(index) ? { fetchPriority: 'high' } : false" :src="tag.media[0].src" class="h-auto w-full rounded-t-md" height="600" + provider="imgproxy" width="400" />🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 106 - 116, The NuxtImg in FeaturedTags.vue is missing the provider attribute; update the NuxtImg element (the one using :src="tag.media[0].src" and isPriorityMedia(index) checks) to include provider="imgproxy" so images use the registered imgproxy optimizer (matching PostMedia.vue) and deliver webp/1x density optimized assets; keep all existing decoding/fetchpriority/loading/preload props intact.pages/posts/[domain].vue (1)
871-882:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winBreadcrumb
itemURLs should use production base.The breadcrumb
itemat Line 875 useslocalePath('/')and Line 879 usesroute.path, producing host-relative URLs. For consistency with the canonical URL pattern (Line 829), build these fromproject.urls.production:defineBreadcrumb({ itemListElement: [ { name: t('nav.home'), - item: localePath('/') + item: new URL(localePath('/'), project.urls.production).href }, { name: t('seo.postsFrom', { domain: selectedBooru.value.domain }), - item: route.path + item: new URL(route.path, project.urls.production).href } ] }),As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains."
🤖 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 `@pages/posts/`[domain].vue around lines 871 - 882, Breadcrumb items currently use host-relative URLs (localePath('/') and route.path); change them to absolute production URLs by prefixing the path with project.urls.production. Specifically, in the defineBreadcrumb call replace localePath('/') and route.path usage with production-based URLs built from project.urls.production combined with the localePath or route path (keeping localized path generation via localePath or route.path but concatenating/constructing an absolute URL using project.urls.production), ensuring the second item still uses selectedBooru.value.domain in its label.
♻️ Duplicate comments (8)
pages/dmca.vue (1)
15-16:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBreadcrumb URLs must be production-absolute for structured data.
The breadcrumb
itemvalues still use host-relative paths vialocalePath(). For structured data consumed by search engines, these must be production-absolute URLs to ensure canonical URLs point toproject.urls.productioneven when served from clone domains.🔧 Proposed fix
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() +const dmcaBreadcrumbUrl = new URL(localePath('/dmca'), project.urls.production).toString() + useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.dmca'), item: localePath('/dmca') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.dmca'), item: dmcaBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: "Use project.urls.production for canonical URLs to point to production even when served from clone domains for SEO".
🤖 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 `@pages/dmca.vue` around lines 15 - 16, The breadcrumb items currently use host-relative paths via localePath('/...') (see the breadcrumb array in pages/dmca.vue); change them to production-absolute URLs by prefixing the localePath result (or the path string) with the canonical base from project.urls.production (obtainable from runtime config or the existing project config) so the breadcrumb.item values become full absolute URLs pointing at project.urls.production + localePath('/...') (or project.urls.production + '/dmca' etc.); update the two entries that use localePath('/') and localePath('/dmca') accordingly.pages/premium/forgot-password.vue (1)
40-48:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute URLs for breadcrumb schema items.
These breadcrumb
itemvalues are host-relative (localePath(...)), so structured data URL identity changes across domains. Build them fromproject.urls.production+ localized path.🔧 Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const premiumBreadcrumbUrl = new URL(localePath('/premium'), project.urls.production).toString() + const forgotPasswordBreadcrumbUrl = new URL(localePath('/premium/forgot-password'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, - { name: t('pages.premium.forgotPassword.seoTitle'), item: localePath('/premium/forgot-password') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.premium.landingPage.seoTitle'), item: premiumBreadcrumbUrl }, + { name: t('pages.premium.forgotPassword.seoTitle'), item: forgotPasswordBreadcrumbUrl } ] }) ])As per coding guidelines: "Use
project.urls.productionfor canonical URLs to point to production (https://r34.app/…) even when served from clone domains for SEO."🤖 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 `@pages/premium/forgot-password.vue` around lines 40 - 48, The breadcrumb schema uses host-relative URLs produced by localePath(...) inside useSchemaOrg/defineBreadcrumb; change the breadcrumb item `item` values to production-absolute URLs by prepending project.urls.production to the localized path (e.g., project.urls.production + localePath('/premium/...') or use a helper that builds absolute URLs from localePath), so defineBreadcrumb receives full canonical URLs rather than host-relative ones.test/server-mocks/plugin.ts (1)
28-29: 🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoffConsider explicit import for clarity.
The defensive fallback pattern is unnecessary since
defineNitroPluginis auto-imported by Nuxt/Nitro. An explicit import would improve clarity, though the current pattern works correctly.🤖 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-mocks/plugin.ts` around lines 28 - 29, Replace the defensive fallback around defineNitroPlugin by explicitly importing defineNitroPlugin instead of using the defineNitroPluginSafe conditional; update references to use the imported defineNitroPlugin (remove the defineNitroPluginSafe variable and its ternary fallback) so the code is clearer and relies on the Nuxt/Nitro auto-import being explicit via import of defineNitroPlugin.pages/settings.vue (1)
32-39:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute breadcrumb URLs in Schema.org.
The breadcrumb
itemvalues are currently relative (localePath(...)), making structured data host-dependent on clone domains. Build absolute URLs fromproject.urls.production.Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const settingsBreadcrumbUrl = new URL(localePath('/settings'), project.urls.production).toString() + useSchemaOrg([ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.settings.title'), item: localePath('/settings') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.settings.title'), item: settingsBreadcrumbUrl } ] }) ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to point to production (https://r34.app/…) even when served from clone domains for SEO.🤖 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 `@pages/settings.vue` around lines 32 - 39, The breadcrumb items use relative URLs from localePath(...) causing host-dependent structured data; update the useSchemaOrg/defineBreadcrumb call so each item.item is an absolute production URL built from project.urls.production plus the localePath return (e.g., join project.urls.production with localePath('/...') or construct a new URL using project.urls.production as base), ensuring correct slashes and locale prefixes are preserved so the Schema.org breadcrumb always points to the canonical production URL.pages/cookie-policy.vue (1)
12-20:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute breadcrumb URLs instead of host-relative locale paths.
The breadcrumb
itemvalues currently uselocalePath(...), which produces host-relative URLs that vary across clone domains. Schema.org structured data should point to a single canonical identity.Proposed fix
+ const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() + const cookiePolicyBreadcrumbUrl = new URL(localePath('/cookie-policy'), project.urls.production).toString() + useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.cookiePolicy'), item: localePath('/cookie-policy') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.cookiePolicy'), item: cookiePolicyBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: Use
project.urls.productionfor canonical URLs to point to production (https://r34.app/…) even when served from clone domains for SEO.🤖 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 `@pages/cookie-policy.vue` around lines 12 - 20, The breadcrumb structured data created in useSchemaOrg via defineBreadcrumb currently uses localePath(...) (host-relative) for item URLs; update the item values to use production-absolute URLs by concatenating project.urls.production with the route path (e.g., project.urls.production + '/' or + '/cookie-policy') so the breadcrumb items emit canonical production URLs; modify the defineBreadcrumb itemListElement entries to stop calling localePath and instead build absolute URLs from project.urls.production while keeping the same display names from t(...).Dockerfile (1)
10-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDefault
SENTRY_UPLOAD_SOURCE_MAPStofalseto prevent build failures.The current default of
truecauses builds to fail when Sentry credentials are not provided. A past review flagged this issue and marked it as addressed, but the code still showsARG SENTRY_UPLOAD_SOURCE_MAPS=true.Proposed fix
-ARG SENTRY_UPLOAD_SOURCE_MAPS=true +ARG SENTRY_UPLOAD_SOURCE_MAPS=falseBased on learnings: Upload source maps to Sentry only in production Docker builds with
SENTRY_ORG,SENTRY_PROJECT,SENTRY_AUTH_TOKENbuild args; setSENTRY_UPLOAD_SOURCE_MAPS=falseto skip.🤖 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 `@Dockerfile` around lines 10 - 16, Change the Docker ARG default for SENTRY_UPLOAD_SOURCE_MAPS from true to false and only enable uploads when the required Sentry build args are present and it's a production Nitro build: set ARG SENTRY_UPLOAD_SOURCE_MAPS=false, keep the ENV export of NITRO_PRESET, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN and SENTRY_UPLOAD_SOURCE_MAPS, and add logic in the build step (or Dockerfile conditional/entrypoint script) to set SENTRY_UPLOAD_SOURCE_MAPS=true only when NITRO_PRESET indicates production and SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN are non-empty.pages/privacy-policy.vue (1)
12-17:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse production-absolute breadcrumb URLs in schema markup.
itemis currently derived fromlocalePath(...)(relative URL). For structured data, these should be production-absolute URLs.🔧 Suggested patch
+const homeBreadcrumbUrl = new URL(localePath('/'), project.urls.production).toString() +const privacyBreadcrumbUrl = new URL(localePath('/privacy-policy'), project.urls.production).toString() + useSchemaOrg(() => [ defineBreadcrumb({ itemListElement: [ - { name: t('nav.home'), item: localePath('/') }, - { name: t('pages.legal.privacyPolicy'), item: localePath('/privacy-policy') } + { name: t('nav.home'), item: homeBreadcrumbUrl }, + { name: t('pages.legal.privacyPolicy'), item: privacyBreadcrumbUrl } ] }), defineWebPage() ])As per coding guidelines: "Use
project.urls.productionfor canonical URLs to point to production (https://r34.app/…) even when served from clone domains for SEO".🤖 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 `@pages/privacy-policy.vue` around lines 12 - 17, The breadcrumb JSON-LD is using localePath(...) which produces relative/local URLs; update the useSchemaOrg callback so defineBreadcrumb's itemListElement items use production-absolute URLs by prefixing the path with project.urls.production (e.g., combine project.urls.production with '/' and the route) instead of localePath; locate the useSchemaOrg(() => [...]) block and adjust the item properties for both t('nav.home') and t('pages.legal.privacyPolicy') to build absolute URLs from project.urls.production.plugins/035.sentry.client.ts (1)
46-84:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't let a transient init failure permanently disable Sentry.
If any import or
Sentry.init()throws, the catch block swallows the error butinitPromisewas already set on line 43. Because the watcher is stopped after the first interaction (line 36), subsequent interactions won't retry initialization.🛠️ Proposed fix to allow retry on failure
async function init() { if (initPromise) return initPromise initPromise = (async () => { try { // ... existing code ... } catch (error) { + initPromise = null // Allow retry on next interaction console.error('Failed to initialize Sentry client', error) } })() return initPromise }Note: The deep import at line 52 is acknowledged as intentional ("Risky on purpose"), but resetting the promise on failure would at least allow recovery from transient network issues.
🤖 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 `@plugins/035.sentry.client.ts` around lines 46 - 84, The try/catch around the Sentry imports and Sentry.init() can leave initPromise filled on failure which prevents future retry (since the interaction watcher stops after first interaction); in the catch block reset the shared initPromise (the variable named initPromise used to track initialization) to undefined/null before logging so subsequent interactions can re-attempt initialization, and keep the console.error for visibility; locate the initPromise variable and update the catch branch that wraps the imports + Sentry.init() to clear it when an error occurs.
🤖 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/layout/navigation/Sidebar.vue`:
- Around line 9-13: The SSR hydration mismatch is caused by running the
display-mode check during SSR in visibilityByLinkId['install-app']; change this
to use a client-only reactive state (like the pattern in
useDialogManagerState.ts) instead of evaluating !import.meta.client inline: add
a ref (e.g., isStandalone or isInstallAllowed) initialized for SSR-safe default,
update it inside a client-only lifecycle hook (onMounted) using
window.matchMedia('(display-mode: standalone)').matches (and listen for changes
if desired), and then have visibleSidebarLinks filter via
visibilityByLinkId[item.id]?.() ?? true that reads that ref so the install-app
visibility is determined only on the client and prevents hydration mismatches.
In `@composables/useLazyToast.ts`:
- Around line 7-20: In loadToast, avoid setting shouldRenderToaster.value = true
before the dynamic import succeeds: move the assignment to after the import
resolves (after await nextTick and after toastPromise resolves) or, if you
prefer keeping it before, reset shouldRenderToaster.value = false in the catch
handler; specifically update the logic around import.meta.client, toastPromise,
and the .catch((error) => { ... }) so that shouldRenderToaster is only true when
the imported toast (from 'vue-sonner') is successfully obtained and remains
false if the import fails.
In `@nuxt.config.js`:
- Around line 190-195: The nuxtLink defaults are defined at the wrong top-level
key; move the existing defaults.nuxtLink block into experimental.defaults so
Nuxt 3 will apply it. Specifically, locate the defaults: { nuxtLink: { prefetch:
false } } entry and relocate that nuxtLink object under the
experimental.defaults object (creating experimental.defaults if missing) so the
setting is experimental.defaults.nuxtLink.prefetch = false.
In `@pages/index.vue`:
- Around line 124-137: The hasStoredPageHistory function currently returns true
on JSON.parse errors which causes corrupted localStorage to be treated as valid;
change the catch branch to return false (and optionally remove the invalid
'settings-pageHistory' key or log the error) so corrupted data doesn't make the
history UI render. Update the catch block in hasStoredPageHistory to return
false and, if desired, call
window.localStorage.removeItem('settings-pageHistory') or console.warn to
surface the corruption.
In `@pages/tags/`[domain]/[tag].vue:
- Line 27: selectedBooru currently force-casts the result of
booruList.value.find(...) to Domain which can be undefined before route
validation runs; change the computed to handle the missing case explicitly by
removing the unsafe "as Domain" cast and either returning a nullable type (e.g.,
Domain | null) or throwing an error inside the computed when the booru isn't
found, and adjust call sites to handle the nullable return or rely on the thrown
error; reference the selectedBooru computed, booruList, domainParam and the
validate timing when implementing the fallback/throw.
---
Outside diff comments:
In `@components/pages/home/FeaturedTags.vue`:
- Around line 106-116: The NuxtImg in FeaturedTags.vue is missing the provider
attribute; update the NuxtImg element (the one using :src="tag.media[0].src" and
isPriorityMedia(index) checks) to include provider="imgproxy" so images use the
registered imgproxy optimizer (matching PostMedia.vue) and deliver webp/1x
density optimized assets; keep all existing
decoding/fetchpriority/loading/preload props intact.
In `@pages/posts/`[domain].vue:
- Around line 871-882: Breadcrumb items currently use host-relative URLs
(localePath('/') and route.path); change them to absolute production URLs by
prefixing the path with project.urls.production. Specifically, in the
defineBreadcrumb call replace localePath('/') and route.path usage with
production-based URLs built from project.urls.production combined with the
localePath or route path (keeping localized path generation via localePath or
route.path but concatenating/constructing an absolute URL using
project.urls.production), ensuring the second item still uses
selectedBooru.value.domain in its label.
---
Duplicate comments:
In `@Dockerfile`:
- Around line 10-16: Change the Docker ARG default for SENTRY_UPLOAD_SOURCE_MAPS
from true to false and only enable uploads when the required Sentry build args
are present and it's a production Nitro build: set ARG
SENTRY_UPLOAD_SOURCE_MAPS=false, keep the ENV export of NITRO_PRESET,
SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN and SENTRY_UPLOAD_SOURCE_MAPS, and
add logic in the build step (or Dockerfile conditional/entrypoint script) to set
SENTRY_UPLOAD_SOURCE_MAPS=true only when NITRO_PRESET indicates production and
SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN are non-empty.
In `@pages/cookie-policy.vue`:
- Around line 12-20: The breadcrumb structured data created in useSchemaOrg via
defineBreadcrumb currently uses localePath(...) (host-relative) for item URLs;
update the item values to use production-absolute URLs by concatenating
project.urls.production with the route path (e.g., project.urls.production + '/'
or + '/cookie-policy') so the breadcrumb items emit canonical production URLs;
modify the defineBreadcrumb itemListElement entries to stop calling localePath
and instead build absolute URLs from project.urls.production while keeping the
same display names from t(...).
In `@pages/dmca.vue`:
- Around line 15-16: The breadcrumb items currently use host-relative paths via
localePath('/...') (see the breadcrumb array in pages/dmca.vue); change them to
production-absolute URLs by prefixing the localePath result (or the path string)
with the canonical base from project.urls.production (obtainable from runtime
config or the existing project config) so the breadcrumb.item values become full
absolute URLs pointing at project.urls.production + localePath('/...') (or
project.urls.production + '/dmca' etc.); update the two entries that use
localePath('/') and localePath('/dmca') accordingly.
In `@pages/premium/forgot-password.vue`:
- Around line 40-48: The breadcrumb schema uses host-relative URLs produced by
localePath(...) inside useSchemaOrg/defineBreadcrumb; change the breadcrumb item
`item` values to production-absolute URLs by prepending project.urls.production
to the localized path (e.g., project.urls.production +
localePath('/premium/...') or use a helper that builds absolute URLs from
localePath), so defineBreadcrumb receives full canonical URLs rather than
host-relative ones.
In `@pages/privacy-policy.vue`:
- Around line 12-17: The breadcrumb JSON-LD is using localePath(...) which
produces relative/local URLs; update the useSchemaOrg callback so
defineBreadcrumb's itemListElement items use production-absolute URLs by
prefixing the path with project.urls.production (e.g., combine
project.urls.production with '/' and the route) instead of localePath; locate
the useSchemaOrg(() => [...]) block and adjust the item properties for both
t('nav.home') and t('pages.legal.privacyPolicy') to build absolute URLs from
project.urls.production.
In `@pages/settings.vue`:
- Around line 32-39: The breadcrumb items use relative URLs from localePath(...)
causing host-dependent structured data; update the useSchemaOrg/defineBreadcrumb
call so each item.item is an absolute production URL built from
project.urls.production plus the localePath return (e.g., join
project.urls.production with localePath('/...') or construct a new URL using
project.urls.production as base), ensuring correct slashes and locale prefixes
are preserved so the Schema.org breadcrumb always points to the canonical
production URL.
In `@plugins/035.sentry.client.ts`:
- Around line 46-84: The try/catch around the Sentry imports and Sentry.init()
can leave initPromise filled on failure which prevents future retry (since the
interaction watcher stops after first interaction); in the catch block reset the
shared initPromise (the variable named initPromise used to track initialization)
to undefined/null before logging so subsequent interactions can re-attempt
initialization, and keep the console.error for visibility; locate the
initPromise variable and update the catch branch that wraps the imports +
Sentry.init() to clear it when an error occurs.
In `@test/server-mocks/plugin.ts`:
- Around line 28-29: Replace the defensive fallback around defineNitroPlugin by
explicitly importing defineNitroPlugin instead of using the
defineNitroPluginSafe conditional; update references to use the imported
defineNitroPlugin (remove the defineNitroPluginSafe variable and its ternary
fallback) so the code is clearer and relies on the Nuxt/Nitro auto-import being
explicit via import of defineNitroPlugin.
🪄 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: 36dfeffb-2781-48b5-a522-c6806c8c0c9a
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (84)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/pt.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/server/path.test.ts
💤 Files with no reviewable changes (4)
- test/pages/premium/backup.test.ts
- server/plugins/lcp-preload-fetchpriority.ts
- app.vue
- app/router.options.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Register components without path prefix using
pathPrefix: falsein Nuxt config; import them as<DomainSelector>not<Input/DomainSelector>
Files:
components/pages/home/Newsletter.vuecomposables/navigation/useMenu.tstest/server/path.test.tspages/terms-of-service.vuepages/other-sites.vuecomposables/useFaviconUrl.tsplugins/050.formbricks.client.tspages/dmca.vueplugins/040.matomo.client.tscomponents/pages/posts/post/PostShareFallback.vuecomponents/layout/modal/ReviewPrompt.vueserver/api/_sitemap-urls.tscomponents/pages/posts/post/PostTagsToggleFallback.vuepages/premium/forgot-password.vuecomposables/useDialogManagerState.tsassets/js/nuxt-image/imgproxy.provider.tscomposables/useIdleTask.tscomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuetest/assets/router-helper.test.tspages/legal.vuetest/server/fix-canonical-queries.test.tspages/cookie-policy.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/PostsPageFooter.vuepages/settings.vuetest/server-mocks/plugin.tscomponents/pages/posts/post/PostSaveFallback.vuetest/helper.tspages/privacy-policy.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/post/PostChatWithAiFallback.vueconfig/i18n.tscomponents/pages/posts/post/PostSource.vuecomponents/shared/ContentContainer.vueassets/js/sidebarLinks.tscomposables/useInteractionDetector.tscomponents/pages/posts/navigation/DomainSelector.vuelayouts/default.vuepages/premium/backup.vuecomponents/pages/posts/post/PostTag.vuecomposables/useLazyToast.tspages/index.vueassets/js/RouterHelper.tscomponents/layout/DialogManager.vuepages/premium/tag-collections.vuecomponents/layout/ClientToaster.vuepages/premium/dashboard.vueplugins/035.sentry.client.tspages/premium/saved-posts/[domain].vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/post/PostComponent.vuepages/tags/[domain]/[tag].vuecomponents/layout/navigation/Navbar.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/index.vuepages/premium/additional-boorus.vuetest/pages/posts.test.tscomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use custom
imgproxyprovider registered for<NuxtImg>with webp format at 1x density only to reduce bandwidthUse
@nuxt/imagev2 module API for image preload priority withpreload: { fetchPriority: 'high' }instead of patching rendered HTML in NitroKeep
@formkit/auto-animateroute-scoped unless it is used broadly; use localvAutoAnimateimports on premium CSR pages instead of global directive
Files:
components/pages/home/Newsletter.vuepages/terms-of-service.vuepages/other-sites.vuepages/dmca.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuepages/premium/forgot-password.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuepages/legal.vuepages/cookie-policy.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/PostsPageFooter.vuepages/settings.vuecomponents/pages/posts/post/PostSaveFallback.vuepages/privacy-policy.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/navigation/DomainSelector.vuelayouts/default.vuepages/premium/backup.vuecomponents/pages/posts/post/PostTag.vuepages/index.vuecomponents/layout/DialogManager.vuepages/premium/tag-collections.vuecomponents/layout/ClientToaster.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuecomponents/pages/home/FeaturedTags.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/post/PostComponent.vuepages/tags/[domain]/[tag].vuecomponents/layout/navigation/Navbar.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/index.vuepages/premium/additional-boorus.vuecomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
{.prettierrc*,**/*.{ts,tsx,js,vue,css}}
📄 CodeRabbit inference engine (AGENTS.md)
Configure Prettier with 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates
Files:
components/pages/home/Newsletter.vuecomposables/navigation/useMenu.tstest/server/path.test.tspages/terms-of-service.vuepages/other-sites.vuecomposables/useFaviconUrl.tsplugins/050.formbricks.client.tspages/dmca.vueplugins/040.matomo.client.tscomponents/pages/posts/post/PostShareFallback.vuecomponents/layout/modal/ReviewPrompt.vueserver/api/_sitemap-urls.tscomponents/pages/posts/post/PostTagsToggleFallback.vuepages/premium/forgot-password.vuecomposables/useDialogManagerState.tsassets/js/nuxt-image/imgproxy.provider.tscomposables/useIdleTask.tscomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/premium/sign-in.vuetest/assets/router-helper.test.tspages/legal.vuetest/server/fix-canonical-queries.test.tspages/cookie-policy.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/PostsPageFooter.vuepages/settings.vuetest/server-mocks/plugin.tscomponents/pages/posts/post/PostSaveFallback.vuetest/helper.tspages/privacy-policy.vuecomponents/pages/home/PageHistorySection.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/post/PostChatWithAiFallback.vueconfig/i18n.tscomponents/pages/posts/post/PostSource.vuecomponents/shared/ContentContainer.vueassets/js/sidebarLinks.tscomposables/useInteractionDetector.tscomponents/pages/posts/navigation/DomainSelector.vuelayouts/default.vuepages/premium/backup.vuecomponents/pages/posts/post/PostTag.vuecomposables/useLazyToast.tspages/index.vueassets/js/RouterHelper.tscomponents/layout/DialogManager.vuepages/premium/tag-collections.vuecomponents/layout/ClientToaster.vuepages/premium/dashboard.vueplugins/035.sentry.client.tspages/premium/saved-posts/[domain].vuecomponents/pages/home/FeaturedTags.vuenuxt.config.jscomponents/shared/ShareButton.vuecomponents/pages/posts/post/PostComponent.vuepages/tags/[domain]/[tag].vuecomponents/layout/navigation/Navbar.vuecomponents/pages/posts/navigation/search/TagCollections.vuepages/premium/index.vuepages/premium/additional-boorus.vuetest/pages/posts.test.tscomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vue
{pages/**/*.vue,composables/**/*.ts}
📄 CodeRabbit inference engine (AGENTS.md)
Use flat bracket keys for query filters (
filter[sort],filter[rating], etc.) with Vue Router's default query handling; do not re-addqsunless the URL contract changes
Files:
composables/navigation/useMenu.tspages/terms-of-service.vuepages/other-sites.vuecomposables/useFaviconUrl.tspages/dmca.vuepages/premium/forgot-password.vuecomposables/useDialogManagerState.tscomposables/useIdleTask.tspages/premium/sign-in.vuepages/legal.vuepages/cookie-policy.vuepages/settings.vuepages/privacy-policy.vuecomposables/useInteractionDetector.tspages/premium/backup.vuecomposables/useLazyToast.tspages/index.vuepages/premium/tag-collections.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuepages/tags/[domain]/[tag].vuepages/premium/index.vuepages/premium/additional-boorus.vuepages/posts/[domain].vue
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer
URL.canParse()orURL.parse()over constructortry/catch; useURL.parse()when the parsed object is needed withURL.canParse()fallback in browser code if compatibility matters
Files:
composables/navigation/useMenu.tstest/server/path.test.tscomposables/useFaviconUrl.tsplugins/050.formbricks.client.tsplugins/040.matomo.client.tsserver/api/_sitemap-urls.tscomposables/useDialogManagerState.tsassets/js/nuxt-image/imgproxy.provider.tscomposables/useIdleTask.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/server-mocks/plugin.tstest/helper.tsconfig/i18n.tsassets/js/sidebarLinks.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsassets/js/RouterHelper.tsplugins/035.sentry.client.tsnuxt.config.jstest/pages/posts.test.ts
test/**/*.{test,spec}.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright insidedescribeblocks that callawait setup({ browser: true })
Files:
test/server/path.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
{nuxt.config.{ts,js},test/**/*.ts}
📄 CodeRabbit inference engine (AGENTS.md)
Mock server-side API calls via test-only Nitro plugin at
test/server-mocks/plugin.ts, injected throughnuxt.config.js→$test.nitro.plugins
Files:
test/server/path.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/server-mocks/plugin.tstest/helper.tsnuxt.config.jstest/pages/posts.test.ts
test/**/*.{test,spec}.ts
📄 CodeRabbit inference engine (AGENTS.md)
Import
debugBrowserOptionsfromtest/helper.tsfor headful playback with slowMo in debug mode
Files:
test/server/path.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
{nuxt.config.{ts,js},pages/**/*.vue,app.vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use
project.urls.productionfor canonical URLs to point to production (https://r34.app/…) even when served from clone domains for SEO
Files:
pages/terms-of-service.vuepages/other-sites.vuepages/dmca.vuepages/premium/forgot-password.vuepages/premium/sign-in.vuepages/legal.vuepages/cookie-policy.vuepages/settings.vuepages/privacy-policy.vuepages/premium/backup.vuepages/index.vuepages/premium/tag-collections.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuenuxt.config.jspages/tags/[domain]/[tag].vuepages/premium/index.vuepages/premium/additional-boorus.vuepages/posts/[domain].vue
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Implement page-specific SEO tags (title, description) using
useSeoMetain the page component
Files:
pages/terms-of-service.vuepages/other-sites.vuepages/dmca.vuepages/premium/forgot-password.vuepages/premium/sign-in.vuepages/legal.vuepages/cookie-policy.vuepages/settings.vuepages/privacy-policy.vuepages/premium/backup.vuepages/index.vuepages/premium/tag-collections.vuepages/premium/dashboard.vuepages/premium/saved-posts/[domain].vuepages/tags/[domain]/[tag].vuepages/premium/index.vuepages/premium/additional-boorus.vuepages/posts/[domain].vue
nuxt.config.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Define locales in
config/i18n.tsas the single source of truth; implement URL prefixes for non-default locales (ru, es, ja) viamirroredRouteRules()helperPlace static global SEO tags (favicon, rating, monetization, color-scheme) in
nuxt.config.jshead.metaKeep
features.inlineStyles: falsein Nuxt config unless new measurements justify revisiting itDo not add service worker logic; keep the service worker intentionally disabled with
selfDestroying: trueIn test mode, set
$test.runtimeConfig.public.apiUrlto''so$fetch(baseURL: '')routes to the local Nitro test serverDisable Sentry fully in tests via
$test.sentry.enabled: falseinnuxt.config.js
Files:
nuxt.config.js
{assets/css/main.css,tailwind.config.js,nuxt.config.{ts,js}}
📄 CodeRabbit inference engine (AGENTS.md)
Use TailwindCSS v4 CSS-based config at
assets/css/main.css, NOT PostCSS; keeptailwind.config.jsonly for the@headlessui/tailwindcssplugin
Files:
nuxt.config.js
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Validate image delivery in an environment where imgproxy can resolve the source URL for non-premium SPA navigations that use direct image paths
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Prefer high-impact, measurable performance optimizations over small rewrites; verify substantial changes with production build, tests, request traces, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Do not add app-level HTML compression unless a direct-origin deployment needs it and the change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Keep the global TanStack Vue Query plugin unless a measured payoff appears for refactoring
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Upload source maps to Sentry only in production Docker builds with `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN` build args; set `SENTRY_UPLOAD_SOURCE_MAPS=false` to skip
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Use `pnpm install --frozen-lockfile` in Docker build stage for production builds; set `SENTRY_UPLOAD_SOURCE_MAPS=false` to skip source map uploads
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T15:35:02.278Z
Learning: Use `NITRO_PRESET` build arg in Docker multi-stage build to select the deployment target
| const visibilityByLinkId = { | ||
| 'install-app': () => !import.meta.client || !window.matchMedia('(display-mode: standalone)').matches | ||
| } | ||
|
|
||
| const visibleSidebarLinks = computed(() => sidebarLinks.filter((item) => visibilityByLinkId[item.id]?.() ?? true)) |
There was a problem hiding this comment.
Hydration mismatch risk: standalone display-mode check runs during SSR.
The visibilityByLinkId['install-app'] function evaluates !import.meta.client during SSR, which returns true and includes the install-app link. On the client in standalone mode, the function returns false and excludes the link, causing a hydration mismatch.
🔧 Proposed fix using reactive state pattern
const localePath = useLocalePath()
const { isPremium } = useUserData()
const { seasonalEmoji } = useSeasonalIcon()
- const visibilityByLinkId = {
- 'install-app': () => !import.meta.client || !window.matchMedia('(display-mode: standalone)').matches
- }
+ const isStandaloneDisplayMode = useState('sidebar-isStandaloneDisplayMode', () => false)
- const visibleSidebarLinks = computed(() => sidebarLinks.filter((item) => visibilityByLinkId[item.id]?.() ?? true))
+ if (import.meta.client) {
+ onMounted(() => {
+ isStandaloneDisplayMode.value = window.matchMedia('(display-mode: standalone)').matches
+ })
+ }
+
+ const visibleSidebarLinks = computed(() =>
+ sidebarLinks.filter((item) => {
+ if (item.id === 'install-app' && isStandaloneDisplayMode.value) {
+ return false
+ }
+ return true
+ })
+ )This pattern matches the approach used in useDialogManagerState.ts (lines 7-13), which correctly handles display-mode detection without hydration mismatches.
🤖 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 `@components/layout/navigation/Sidebar.vue` around lines 9 - 13, The SSR
hydration mismatch is caused by running the display-mode check during SSR in
visibilityByLinkId['install-app']; change this to use a client-only reactive
state (like the pattern in useDialogManagerState.ts) instead of evaluating
!import.meta.client inline: add a ref (e.g., isStandalone or isInstallAllowed)
initialized for SSR-safe default, update it inside a client-only lifecycle hook
(onMounted) using window.matchMedia('(display-mode: standalone)').matches (and
listen for changes if desired), and then have visibleSidebarLinks filter via
visibilityByLinkId[item.id]?.() ?? true that reads that ref so the install-app
visibility is determined only on the client and prevents hydration mismatches.
| function hasStoredPageHistory() { | ||
| const rawPageHistory = window.localStorage.getItem('settings-pageHistory') | ||
|
|
||
| if (!rawPageHistory || rawPageHistory === '[]') { | ||
| return false | ||
| } | ||
|
|
||
| try { | ||
| const pageHistory = JSON.parse(rawPageHistory) | ||
| return Array.isArray(pageHistory) ? pageHistory.length > 0 : Boolean(pageHistory) | ||
| } catch { | ||
| return true | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Graceful fallback on parse error may hide corrupted data.
Line 135 returns true when JSON parsing fails, which causes the history section to load even with corrupted localStorage. Consider returning false instead to avoid rendering broken state.
Suggested fix
try {
const pageHistory = JSON.parse(rawPageHistory)
return Array.isArray(pageHistory) ? pageHistory.length > 0 : Boolean(pageHistory)
} catch {
- return true
+ return false
}📝 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.
| function hasStoredPageHistory() { | |
| const rawPageHistory = window.localStorage.getItem('settings-pageHistory') | |
| if (!rawPageHistory || rawPageHistory === '[]') { | |
| return false | |
| } | |
| try { | |
| const pageHistory = JSON.parse(rawPageHistory) | |
| return Array.isArray(pageHistory) ? pageHistory.length > 0 : Boolean(pageHistory) | |
| } catch { | |
| return true | |
| } | |
| } | |
| function hasStoredPageHistory() { | |
| const rawPageHistory = window.localStorage.getItem('settings-pageHistory') | |
| if (!rawPageHistory || rawPageHistory === '[]') { | |
| return false | |
| } | |
| try { | |
| const pageHistory = JSON.parse(rawPageHistory) | |
| return Array.isArray(pageHistory) ? pageHistory.length > 0 : Boolean(pageHistory) | |
| } catch { | |
| return false | |
| } | |
| } |
🤖 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 `@pages/index.vue` around lines 124 - 137, The hasStoredPageHistory function
currently returns true on JSON.parse errors which causes corrupted localStorage
to be treated as valid; change the catch branch to return false (and optionally
remove the invalid 'settings-pageHistory' key or log the error) so corrupted
data doesn't make the history UI render. Update the catch block in
hasStoredPageHistory to return false and, if desired, call
window.localStorage.removeItem('settings-pageHistory') or console.warn to
surface the corruption.
ab64628 to
efab159
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
components/pages/home/FeaturedTags.vue (1)
106-117: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winUse the project’s imgproxy contract for
<NuxtImg>.This change keeps the default provider, but the repo rule requires custom imgproxy output at 1x webp for Nuxt images.
As per coding guidelines, "Use custom imgproxy provider for to generate images at 1x density only in webp format to reduce bandwidth".Suggested fix
<NuxtImg + provider="imgproxy" + format="webp" + densities="1x" :alt="t('common.featuredTag', { name: tag.name })" :decoding="isPriorityMedia(index) ? undefined : 'async'" :fetchpriority="isPriorityMedia(index) ? undefined : 'low'" :loading="isPriorityMedia(index) ? 'eager' : 'lazy'" :preload="isPriorityMedia(index) ? { fetchPriority: 'high' } : false" :src="tag.media[0].src"🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 106 - 117, The NuxtImg usage must use the project's imgproxy contract instead of the default provider and force 1x webp output; update the <NuxtImg> in FeaturedTags.vue to specify the project's imgproxy provider and set the img options to produce webp at density=1 (e.g., add the provider binding for the imgproxy implementation and pass/derive format='webp' and density=1 for the src coming from tag.media[0].src), leaving the existing decoding/loading/preload logic (isPriorityMedia) intact and ensuring tag.media is referenced the same way.pages/index.vue (1)
153-170:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix
isPremiumshadowing in the homepage ad watcher.Line 155 shadows the
isPremiumref with a boolean callback value, so the guard at Line 169 does not reliably re-check current premium state before loading ads.Suggested fix
watch( [hasInteracted, isPremium], - ([hasInteracted, isPremium]) => { + ([hasInteracted, isPremiumUser]) => { if (hasLoadedAds.value) { return } if (!hasInteracted) { return } - if (isPremium) { + if (isPremiumUser) { return } scheduleIdleTask(async () => { if (hasLoadedAds.value || isPremium.value) { return }🤖 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 `@pages/index.vue` around lines 153 - 170, The watcher callback is shadowing the isPremium ref by using the same name for the destructured boolean parameter, causing the later guard to read the stale/shadowed boolean instead of the reactive ref; update the watcher signature to rename the destructured parameters (e.g., ([hasInteractedVal, isPremiumFlag]) or similar) and then use the original ref (isPremium.value) when checking current premium state (and keep using hasInteracted.value / hasLoadedAds.value consistently) before calling scheduleIdleTask so the premium guard at the inner check reliably reads the reactive ref.pages/posts/[domain].vue (2)
57-74:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix
isPremiumshadowing in the ad watcher.Line 59 shadows the
isPremiumref with a boolean callback value, so the re-check at Line 73 does not read the reactive premium state before initializing ads.Suggested fix
watch( [hasInteracted, isPremium], - ([hasInteracted, isPremium]) => { + ([hasInteracted, isPremiumUser]) => { if (hasLoadedAds.value) { return } if (!hasInteracted) { return } - if (isPremium) { + if (isPremiumUser) { return } scheduleIdleTask(async () => { if (hasLoadedAds.value || isPremium.value) { return }🤖 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 `@pages/posts/`[domain].vue around lines 57 - 74, The watcher currently shadows the isPremium ref by using the same name in the callback params, causing the later check inside scheduleIdleTask to read the boolean param instead of the reactive ref; update the callback parameter names (e.g., rename the destructured ([hasInteracted, isPremium]) => to ([hasInteractedVal, isPremiumFlag]) => or similar) and ensure all internal checks that need the reactive state use the outer isPremium ref (isPremium.value) while local boolean params are only used for the immediate synchronous checks; verify hasLoadedAds, hasInteracted, isPremium and scheduleIdleTask references remain correct and unshadowed.
54-85: 🧹 Nitpick | 🔵 TrivialValidate the deferred-loading wins on a production build before merge.
Given the amount of idle/deferred work added in this page, please capture before/after request traces and Lighthouse on the built app to confirm measurable LCP/INP gains and no delayed UX regressions.
Based on learnings: "After substantial performance changes, verify with production build, relevant tests, request traces, and Lighthouse against built app before deciding to keep the change".
🤖 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 `@pages/posts/`[domain].vue around lines 54 - 85, Add production-build validation for the deferred ad-loading around onMounted/watch/hasLoadedAds/scheduleIdleTask so we confirm it yields real LCP/INP improvements and no UX regressions: build and deploy the production app, capture before/after request traces and Lighthouse reports (LCP, INP, TTFB, bundle/asset timing) for the page that imports useAdvertisements and for a control run without the change, and compare metrics and flame charts to ensure the idle import via nuxtApp.runWithContext(useAdvertisements) actually reduces main-thread and network work without introducing delayed interactive regressions; if regressions appear, revert or tune the idle scheduling and re-run the same production traces.
♻️ Duplicate comments (2)
layouts/default.vue (1)
30-34:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFullscreen restore can be skipped on exit due to target guard.
At Line 32, non-
Elementtargets are dropped. Onfullscreenchange, the target is oftendocument, so the restore at Line 42 may never run.Suggested fix
- const target = event.target - - if (!(target instanceof Element)) { + const target = + event.target instanceof Element + ? event.target + : document.fullscreenElement ?? (document.activeElement instanceof Element ? document.activeElement : null) + + if (!target) { return }Also applies to: 42-42
🤖 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 `@layouts/default.vue` around lines 30 - 34, The fullscreenchange handler currently exits when event.target is not an Element, which skips restore logic when the target is document; change the guard so it accepts Document as well (or remove the strict instanceof Element check) — e.g., treat event.target || document as the target or check for Node/Document types instead of only Element — and ensure the subsequent restore logic (the fullscreen restore call) runs for document targets as well; locate the handler using the identifiers event, target and the fullscreenchange handler in layouts/default.vue to update the guard and preserve the restore invocation.assets/js/sidebarLinks.ts (1)
35-39:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
project.urls.productionfor the FAQ link.Line 37 hardcodes a domain instead of using the configured production base URL, which can drift from canonical routing behavior.
Suggested fix
{ id: 'faq', nameKey: 'nav.faq', - href: 'https://rule34.app/frequently-asked-questions', + href: new URL('/frequently-asked-questions', project.urls.production).toString(), isExternal: true, icon: QuestionMarkCircleIcon },As per coding guidelines: "Canonical URLs must point to production (https://r34.app/…) even when served from clone domains; use project.urls.production for canonicals".
🤖 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 `@assets/js/sidebarLinks.ts` around lines 35 - 39, The FAQ link currently hardcodes 'https://rule34.app/frequently-asked-questions' in the sidebar entry with id 'faq'; change the href to derive from the configured canonical base by using project.urls.production combined with the FAQ path (e.g., project.urls.production + '/frequently-asked-questions') so the nav.faq canonical uses the production URL; update the sidebar entry where id: 'faq' / href is defined in sidebarLinks.ts and keep isExternal and icon (QuestionMarkCircleIcon) unchanged.
🤖 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/home/FeaturedTags.vue`:
- Around line 30-37: tagsKey is currently derived only from props.domain and
tagsWithMedia.value.map(...).lengths which can collide when tag payloads change
but media lengths remain same, causing stale preselectedTags reuse; update
tagsKey (used when creating preselectedTags via useState and callOnce) to
incorporate a stable fingerprint of the actual tag payloads (e.g., join tag ids
or names or compute a short hash from tagsWithMedia.value.map(tag =>
tag.id||tag.name||JSON.stringify(tag))) so the key changes whenever tag content
changes and prevents reuse of outdated state for preselectedTags.
In `@components/pages/posts/post/PostSource.vue`:
- Around line 78-82: The direct assignment const canParseUrl = URL.canParse is
unsafe on older browsers; replace it with a guarded helper named canParseUrl
(used by getHostnameFromUrl and the template) that checks for URL.parse and
URL.canParse, falls back to trying new URL(input) inside a try/catch, and
returns a boolean—then update getHostnameFromUrl to call this helper (keep its
signature) so hostname extraction and template calls won’t throw on browsers
lacking URL.canParse.
In `@components/pages/posts/post/PostSourceFallback.vue`:
- Around line 6-17: The fallback control in PostSourceFallback.vue is a
permanently non-interactive button (disabled + tabindex="-1") but still exposes
an action aria-label; change it to a purely decorative element so it is removed
from the accessibility tree: replace the <button> wrapper around LinkIcon with a
non-interactive element (e.g., <span> or <div>) and remove the aria-label and
tabindex, ensuring LinkIcon stays aria-hidden="true" (or add aria-hidden="true"
to the wrapper). This keeps the visual styling (classes like "flex items-center
rounded-md px-1.5 py-1") but prevents screen readers from treating it as a
clickable control.
In `@components/shared/ShareButton.vue`:
- Line 13: The toast calls from useLazyToast() can return promises and are
currently fired without awaiting or catching, causing potential unhandled
rejections; update the clipboard success/failure paths (where toast.success(...)
and toast.error(...) are invoked) to await the promises or attach .catch
handlers (e.g., await toast.success(...) or toast.success(...).catch(...))
inside the copy/clipboard handler function so that both success and error toast
calls from useLazyToast() are properly handled.
In `@i18n/locales/pt.json`:
- Around line 160-327: The pt locale is missing the translation key
pages.premium.savedPostsPage.title referenced by
pages/premium/saved-posts/[domain].vue
(t('pages.premium.savedPostsPage.title')), causing fallbacks; add the key under
the premium section in i18n/locales/pt.json (e.g., add savedPostsPage: {
"title": "..." }) with the appropriate Portuguese string, ensuring the JSON
structure matches existing nested keys (pages → premium → savedPostsPage →
title) and run a quick lint/format check to keep the file valid.
In `@pages/premium/dashboard.vue`:
- Line 19: The toast instance from useLazyToast() returns promise-based methods;
update the calls to toast.error(...) and toast.success(...) (the usages in this
file at the spots invoking toast, e.g., around the handlers that call
toast.error and toast.success) to await the returned promises or explicitly
handle them (e.g., await toast.error(...); or toast.error(...).catch(() => {}) )
so any lazy-import failure is handled and avoids unhandled promise rejections;
ensure the functions calling these toasts are marked async if needed (e.g., the
action/handler functions that call toast.error/ success).
In `@README.md`:
- Around line 103-113: Update the README prerequisites to match the new pnpm
workflow: replace any mention of "NPM" with "pnpm", add a short note to install
pnpm (e.g., via npm: `npm install -g pnpm`) or link to pnpm docs, and ensure the
example setup commands (pnpm install, pnpm dev, pnpm build, pnpm generate) are
consistent with the prerequisites section so users aren’t shown conflicting
package manager instructions.
In `@test/pages/posts.test.ts`:
- Line 301: The wheel delta used in the test (the call to page.mouse.wheel(0,
100000) in posts.test.ts) is unreasonably large; either replace the 100000 with
a smaller, more realistic delta that still triggers pagination (e.g., a few
hundred or a loop of smaller wheel events) or add a brief inline comment
immediately above the page.mouse.wheel call explaining why such a large delta is
required for the test environment so future readers understand the choice.
- Around line 6-17: The function decodeImgproxySourceUrl should guard against
malformed base64 causing Buffer.from(paddedBase64, 'base64') to throw; wrap the
decode step in a try/catch inside decodeImgproxySourceUrl (catch any errors from
Buffer.from(...)) and return null (or a clear sentinel) on failure, ensuring
callers get a safe null instead of an uncaught exception.
- Line 506: The test constructs new URL(canonicalHref!) without guarding against
canonicalHref being null (it comes from element.getAttribute), so update the
test to check canonicalHref is not null before creating a URL (e.g., assert/call
expect(canonicalHref).not.toBeNull() or return early) or use optional
chaining/fallback when building the URL; ensure you reference the canonicalHref
variable and the URL construction in the assertion (expect(new
URL(...).pathname).toBe(...)) so the test no longer uses the non-null assertion
operator.
---
Outside diff comments:
In `@components/pages/home/FeaturedTags.vue`:
- Around line 106-117: The NuxtImg usage must use the project's imgproxy
contract instead of the default provider and force 1x webp output; update the
<NuxtImg> in FeaturedTags.vue to specify the project's imgproxy provider and set
the img options to produce webp at density=1 (e.g., add the provider binding for
the imgproxy implementation and pass/derive format='webp' and density=1 for the
src coming from tag.media[0].src), leaving the existing decoding/loading/preload
logic (isPriorityMedia) intact and ensuring tag.media is referenced the same
way.
In `@pages/index.vue`:
- Around line 153-170: The watcher callback is shadowing the isPremium ref by
using the same name for the destructured boolean parameter, causing the later
guard to read the stale/shadowed boolean instead of the reactive ref; update the
watcher signature to rename the destructured parameters (e.g.,
([hasInteractedVal, isPremiumFlag]) or similar) and then use the original ref
(isPremium.value) when checking current premium state (and keep using
hasInteracted.value / hasLoadedAds.value consistently) before calling
scheduleIdleTask so the premium guard at the inner check reliably reads the
reactive ref.
In `@pages/posts/`[domain].vue:
- Around line 57-74: The watcher currently shadows the isPremium ref by using
the same name in the callback params, causing the later check inside
scheduleIdleTask to read the boolean param instead of the reactive ref; update
the callback parameter names (e.g., rename the destructured ([hasInteracted,
isPremium]) => to ([hasInteractedVal, isPremiumFlag]) => or similar) and ensure
all internal checks that need the reactive state use the outer isPremium ref
(isPremium.value) while local boolean params are only used for the immediate
synchronous checks; verify hasLoadedAds, hasInteracted, isPremium and
scheduleIdleTask references remain correct and unshadowed.
- Around line 54-85: Add production-build validation for the deferred ad-loading
around onMounted/watch/hasLoadedAds/scheduleIdleTask so we confirm it yields
real LCP/INP improvements and no UX regressions: build and deploy the production
app, capture before/after request traces and Lighthouse reports (LCP, INP, TTFB,
bundle/asset timing) for the page that imports useAdvertisements and for a
control run without the change, and compare metrics and flame charts to ensure
the idle import via nuxtApp.runWithContext(useAdvertisements) actually reduces
main-thread and network work without introducing delayed interactive
regressions; if regressions appear, revert or tune the idle scheduling and
re-run the same production traces.
---
Duplicate comments:
In `@assets/js/sidebarLinks.ts`:
- Around line 35-39: The FAQ link currently hardcodes
'https://rule34.app/frequently-asked-questions' in the sidebar entry with id
'faq'; change the href to derive from the configured canonical base by using
project.urls.production combined with the FAQ path (e.g.,
project.urls.production + '/frequently-asked-questions') so the nav.faq
canonical uses the production URL; update the sidebar entry where id: 'faq' /
href is defined in sidebarLinks.ts and keep isExternal and icon
(QuestionMarkCircleIcon) unchanged.
In `@layouts/default.vue`:
- Around line 30-34: The fullscreenchange handler currently exits when
event.target is not an Element, which skips restore logic when the target is
document; change the guard so it accepts Document as well (or remove the strict
instanceof Element check) — e.g., treat event.target || document as the target
or check for Node/Document types instead of only Element — and ensure the
subsequent restore logic (the fullscreen restore call) runs for document targets
as well; locate the handler using the identifiers event, target and the
fullscreenchange handler in layouts/default.vue to update the guard and preserve
the restore invocation.
🪄 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: b3865600-8ed0-430e-a4b6-07b5897f1a18
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (85)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useAdvertisements.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/pt.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/server/path.test.ts
💤 Files with no reviewable changes (4)
- test/pages/premium/backup.test.ts
- app.vue
- server/plugins/lcp-preload-fetchpriority.ts
- app/router.options.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{vue,ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt 4 (SSR, Nitro server) with Vue 3 + TypeScript as the primary framework stack
Files:
composables/navigation/useMenu.tscomponents/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostDownloadFallback.vueassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tscomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomposables/useIdleTask.tstest/server/path.test.tsassets/js/RouterHelper.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vueserver/api/_sitemap-urls.tsconfig/i18n.tscomponents/pages/home/PageHistory.vuepages/premium/index.vuepages/premium/sign-in.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomposables/useAdvertisements.tscomponents/pages/posts/post/PostSourceFallback.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuecomponents/pages/posts/post/PostTagsToggleFallback.vueassets/js/sidebarLinks.tstest/server/fix-canonical-queries.test.tspages/premium/forgot-password.vuelayouts/default.vuecomposables/useInteractionDetector.tspages/premium/saved-posts/[domain].vuecomposables/useDialogManagerState.tspages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostSource.vuecomposables/useLazyToast.tspages/premium/dashboard.vuecomponents/pages/posts/navigation/search/TagCollections.vueplugins/035.sentry.client.tscomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuepages/posts/[domain].vuepages/premium/tag-collections.vuetest/server-mocks/plugin.tscomponents/pages/posts/navigation/DomainSelector.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/additional-boorus.vuetest/assets/router-helper.test.tscomponents/pages/posts/post/PostMedia.vuetest/helper.tspages/tags/[domain]/[tag].vuetest/pages/posts.test.ts
**/*.{ts,tsx,js,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:
composables/navigation/useMenu.tscomponents/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostDownloadFallback.vueassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tscomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomposables/useIdleTask.tstest/server/path.test.tsassets/js/RouterHelper.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vuei18n/locales/ja.jsonserver/api/_sitemap-urls.tsi18n/locales/es.jsonconfig/i18n.tsi18n/locales/ru.jsoncomponents/pages/home/PageHistory.vuepages/premium/index.vuepages/premium/sign-in.vuecomponents/shared/ContentContainer.vuei18n/locales/fr.jsoncomponents/pages/posts/post/PostTag.vuecomposables/useAdvertisements.tscomponents/pages/posts/post/PostSourceFallback.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuecomponents/pages/posts/post/PostTagsToggleFallback.vueassets/js/sidebarLinks.tsi18n/locales/en.jsontest/server/fix-canonical-queries.test.tspages/premium/forgot-password.vuei18n/locales/de.jsonlayouts/default.vuei18n/locales/pt.jsoncomposables/useInteractionDetector.tspages/premium/saved-posts/[domain].vuecomposables/useDialogManagerState.tspages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostSource.vuecomposables/useLazyToast.tspages/premium/dashboard.vuecomponents/pages/posts/navigation/search/TagCollections.vueplugins/035.sentry.client.tscomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuepages/posts/[domain].vuepages/premium/tag-collections.vuetest/server-mocks/plugin.tscomponents/pages/posts/navigation/DomainSelector.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepackage.jsonpages/premium/additional-boorus.vuetest/assets/router-helper.test.tscomponents/pages/posts/post/PostMedia.vuenuxt.config.jstest/helper.tspages/tags/[domain]/[tag].vuetest/pages/posts.test.ts
**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Canonical URLs must point to production (https://r34.app/…) even when served from clone domains; use project.urls.production for canonicals
Schema.org breadcrumb item URLs should stay local/locale-relative; do not convert breadcrumb items to project.urls.production
Query filters must use flat bracket keys (filter[sort], filter[rating], etc.) with Vue Router's default query handling; do not re-add qs unless URL contract changes
External API is called at NUXT_PUBLIC_API_URL (default http://localhost:8081); API codebase is at github.com/Rule-34/API
Files:
composables/navigation/useMenu.tscomponents/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomposables/useFaviconUrl.tscomponents/pages/posts/post/PostDownloadFallback.vueassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tscomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomposables/useIdleTask.tstest/server/path.test.tsassets/js/RouterHelper.tscomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vueserver/api/_sitemap-urls.tsconfig/i18n.tscomponents/pages/home/PageHistory.vuepages/premium/index.vuepages/premium/sign-in.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomposables/useAdvertisements.tscomponents/pages/posts/post/PostSourceFallback.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuecomponents/pages/posts/post/PostTagsToggleFallback.vueassets/js/sidebarLinks.tstest/server/fix-canonical-queries.test.tspages/premium/forgot-password.vuelayouts/default.vuecomposables/useInteractionDetector.tspages/premium/saved-posts/[domain].vuecomposables/useDialogManagerState.tspages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostSource.vuecomposables/useLazyToast.tspages/premium/dashboard.vuecomponents/pages/posts/navigation/search/TagCollections.vueplugins/035.sentry.client.tscomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuepages/posts/[domain].vuepages/premium/tag-collections.vuetest/server-mocks/plugin.tscomponents/pages/posts/navigation/DomainSelector.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/additional-boorus.vuetest/assets/router-helper.test.tscomponents/pages/posts/post/PostMedia.vuetest/helper.tspages/tags/[domain]/[tag].vuetest/pages/posts.test.ts
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch; use URL.parse() when parsed URL object is needed with URL.canParse() fallback in browser code if compatibility matters
Files:
composables/navigation/useMenu.tscomposables/useFaviconUrl.tsassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tscomposables/useIdleTask.tstest/server/path.test.tsassets/js/RouterHelper.tsserver/api/_sitemap-urls.tsconfig/i18n.tscomposables/useAdvertisements.tsassets/js/sidebarLinks.tstest/server/fix-canonical-queries.test.tscomposables/useInteractionDetector.tscomposables/useDialogManagerState.tsplugins/050.formbricks.client.tscomposables/useLazyToast.tsplugins/035.sentry.client.tstest/server-mocks/plugin.tstest/assets/router-helper.test.tsnuxt.config.jstest/helper.tstest/pages/posts.test.ts
**/*.{css,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use TailwindCSS v4 via
@tailwindcss/viteVite plugin (NOT PostCSS) with CSS-based config in assets/css/main.css
Files:
components/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/PageHistory.vuepages/premium/index.vuepages/premium/sign-in.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostSourceFallback.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuepages/premium/forgot-password.vuelayouts/default.vuepages/premium/saved-posts/[domain].vuepages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSource.vuepages/premium/dashboard.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuepages/posts/[domain].vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/additional-boorus.vuecomponents/pages/posts/post/PostMedia.vuepages/tags/[domain]/[tag].vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Components must be auto-imported without path prefix using syntax, not <Folder/ComponentName>
Files:
components/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/PageHistory.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostMedia.vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use custom imgproxy provider for to generate images at 1x density only in webp format to reduce bandwidth
Use
@nuxt/imagev2 preload API with fetchPriority for image preload priority instead of patching rendered HTML in NitroKeep
@formkit/auto-animateroute-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages instead of global directive
Files:
components/pages/home/Newsletter.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/navigation/Navbar.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/PageHistory.vuepages/premium/index.vuepages/premium/sign-in.vuecomponents/shared/ContentContainer.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostSourceFallback.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vuepages/privacy-policy.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuepages/premium/forgot-password.vuelayouts/default.vuepages/premium/saved-posts/[domain].vuepages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/post/PostSource.vuepages/premium/dashboard.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/shared/ShareButton.vuecomponents/layout/DialogManager.vuecomponents/layout/navigation/SidebarWrapper.vuepages/posts/[domain].vuepages/premium/tag-collections.vuecomponents/pages/posts/navigation/DomainSelector.vuepages/index.vuecomponents/pages/posts/post/PostComponent.vuepages/premium/additional-boorus.vuecomponents/pages/posts/post/PostMedia.vuepages/tags/[domain]/[tag].vue
Dockerfile
📄 CodeRabbit inference engine (AGENTS.md)
Source map uploads to Sentry only happen in production Docker builds; requires SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN build args
Docker production build multi-stage: build stage uses pnpm install --frozen-lockfile; production stage copies only .output/ (no node_modules needed — Nitro bundles everything)
Set NITRO_PRESET build arg to select the deployment target in Docker production build
Files:
Dockerfile
test/**/*.{test,spec}.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest +
@nuxt/test-utilswith Playwright browser mode for testing
Files:
test/server/path.test.tstest/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
test/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Tests with
@nuxt/test-utilsmust call await setup({ browser: true }) inside describe blocks for Playwright browser testingServer-side API calls must be mocked via test-only Nitro plugin at test/server-mocks/plugin.ts, injected through nuxt.config.js → $test.nitro.plugins
For test debug mode, import debugBrowserOptions from test/helper.ts for headful playback with slowMo
Files:
test/server/path.test.tstest/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
config/i18n.ts
📄 CodeRabbit inference engine (AGENTS.md)
Locales must be defined in config/i18n.ts (single source of truth) with non-default locales (ru, es, ja) getting URL prefixes
Files:
config/i18n.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Page-specific SEO tags (title, description) should use useSeoMeta in the page component
Files:
pages/premium/index.vuepages/premium/sign-in.vuepages/other-sites.vuepages/legal.vuepages/dmca.vuepages/privacy-policy.vuepages/premium/forgot-password.vuepages/premium/saved-posts/[domain].vuepages/premium/backup.vuepages/terms-of-service.vuepages/settings.vuepages/cookie-policy.vuepages/premium/dashboard.vuepages/posts/[domain].vuepages/premium/tag-collections.vuepages/index.vuepages/premium/additional-boorus.vuepages/tags/[domain]/[tag].vue
package.json
📄 CodeRabbit inference engine (AGENTS.md)
Node ≥ 24 required per package.json engines field
pnpm ≥ 11.1 required per packageManager field in package.json
Files:
package.json
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless replacing has clear, measured payoff
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: After substantial performance changes, verify with production build, relevant tests, request traces, and Lighthouse against built app before deciding to keep the change
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: Production is behind Cloudflare with Brotli compression; do not add app-level HTML compression unless direct-origin deployment needs it and change is verified with headers, byte sizes, warm TTFB, and Lighthouse
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: Keep the global TanStack Vue Query plugin unless larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: Service worker is intentionally disabled (selfDestroying: true); do not add service worker logic
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:16:42.833Z
Learning: Git submodule at assets/lib/rule-34-shared-resources must be cloned with --recursive flag
🔇 Additional comments (80)
composables/navigation/useMenu.ts (1)
1-1: LGTM!components/pages/home/Newsletter.vue (1)
45-45: LGTM!components/pages/posts/post/PostSaveFallback.vue (1)
6-17: LGTM!components/layout/modal/ReviewPrompt.vue (1)
44-50: LGTM!components/pages/posts/PostsPageFooter.vue (2)
1-6: LGTM!
125-125: LGTM!components/layout/ClientToaster.vue (1)
1-13: LGTM!components/pages/posts/post/PostShareFallback.vue (1)
1-18: LGTM!composables/useFaviconUrl.ts (1)
1-7: LGTM!components/pages/posts/post/PostDownloadFallback.vue (1)
1-18: LGTM!assets/js/nuxt-image/imgproxy.provider.ts (1)
41-41: LGTM!Dockerfile (1)
20-26: LGTM!plugins/040.matomo.client.ts (1)
41-47: LGTM!components/pages/posts/post/PostChatWithAiFallback.vue (1)
1-20: LGTM!components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-30
.vscode/settings.json (1)
1-3: LGTM!public/llms.txt (1)
9-9: No changes needed. The documentation example is correct.The code confirms that tags use the
?tags=query parameter format (notfilter[tags]). RouterHelper.ts line 27 setsroute.query.tags = tags.map((tag) => tag.name).join('|'), and the posts page directly accesses this parameter on multiple lines. The "flat bracket keys" guideline applies only to filter parameters (sort, rating, etc.), not to tags, which have their own parameter with pipe-separated values.composables/useIdleTask.ts (1)
1-16: LGTM!test/server/path.test.ts (1)
4-4: LGTM!assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-46, 62-70
components/layout/navigation/Navbar.vue (1)
2-3: LGTM!Also applies to: 21-31
i18n/locales/ja.json (1)
389-390: LGTM!Also applies to: 404-404
pnpm-workspace.yaml (1)
1-6: ⚡ Quick winThe
allowBuildsconfiguration is the correct and current pnpm key for managing dependency build scripts (v10.26.0+). This is the modern replacement for older settings likeonlyBuiltDependencies. The YAML shape in your code is correct; packages not listed are disallowed by default whenstrictDepBuildsis enabled (the pnpm default).No action needed.
server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 17-17, 22-22
i18n/locales/es.json (1)
389-390: LGTM!Also applies to: 404-404
config/i18n.ts (1)
9-12: LGTM!i18n/locales/ru.json (1)
389-390: LGTM!Also applies to: 404-404
components/pages/home/PageHistory.vue (1)
3-3: LGTM!Also applies to: 35-35
pages/premium/index.vue (1)
177-177: LGTM!Also applies to: 181-181, 202-207, 666-666, 668-669
pages/premium/sign-in.vue (1)
8-8: LGTM!Also applies to: 61-69
components/shared/ContentContainer.vue (1)
2-2: LGTM!Also applies to: 26-26, 35-35, 48-54, 56-56
i18n/locales/fr.json (1)
1-539: LGTM!components/pages/posts/post/PostTag.vue (1)
2-15: LGTM!Also applies to: 31-31
composables/useAdvertisements.ts (1)
74-82: LGTM!pages/other-sites.vue (1)
5-5: LGTM!Also applies to: 24-31
pages/legal.vue (1)
12-21: LGTM!pages/dmca.vue (1)
5-5: LGTM!Also applies to: 12-21, 50-52
components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!pages/privacy-policy.vue (1)
12-20: LGTM!components/pages/posts/post/PostTagsToggleFallback.vue (1)
1-19: LGTM!assets/js/sidebarLinks.ts (1)
12-33: LGTM!Also applies to: 42-61
i18n/locales/en.json (1)
389-390: LGTM!Also applies to: 404-404
test/server/fix-canonical-queries.test.ts (1)
14-15: LGTM!Also applies to: 24-24, 33-33, 36-37, 57-57
pages/premium/forgot-password.vue (1)
5-5: LGTM!Also applies to: 40-49
i18n/locales/de.json (1)
1-540: LGTM!layouts/default.vue (1)
2-7: LGTM!Also applies to: 91-97
composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-18
pages/premium/saved-posts/[domain].vue (1)
13-13: LGTM!Also applies to: 22-22, 33-38, 91-91, 97-97, 114-117, 232-232, 618-626
composables/useDialogManagerState.ts (1)
1-65: LGTM!pages/premium/backup.vue (1)
9-9: LGTM!Also applies to: 63-65, 71-79, 127-127
pages/terms-of-service.vue (1)
5-5: LGTM!Also applies to: 12-20
pages/settings.vue (1)
9-10: LGTM!Also applies to: 32-40
pages/cookie-policy.vue (1)
5-5: LGTM!Also applies to: 12-20
components/layout/navigation/Sidebar.vue (1)
9-13: Hydration mismatch risk remains in install-app visibility logic.This matches the earlier finding: Line 10 can render differently between SSR and client (standalone mode), causing inconsistent initial markup.
components/pages/posts/navigation/search/TagCollections.vue (1)
17-17: Async lazy toast usage issue still applies in this handler.This matches the previously reported concern: after switching to
useLazyToast(),toast.error(...)calls increateTagCollectionFromSelectedTagsshould be awaited/handled.Also applies to: 35-52
plugins/035.sentry.client.ts (2)
47-53: Private Sentry runtime deep-import concern remains.This is the same previously flagged issue: importing
@sentry/nuxtinternals via deep runtime path is fragile across package updates/exports.
82-84: Init failure retry gap remains.This matches the earlier finding: failure in
init()is swallowed without resetting the init sentinel, so retries may be blocked for the session.plugins/050.formbricks.client.ts (1)
2-3: LGTM!Also applies to: 25-33
composables/useLazyToast.ts (1)
1-53: LGTM!components/layout/DialogManager.vue (1)
2-77: LGTM!Also applies to: 88-121
components/layout/navigation/SidebarWrapper.vue (1)
5-10: LGTM!Also applies to: 17-109
pages/premium/tag-collections.vue (1)
3-12: LGTM!Also applies to: 147-155, 173-174
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-139
components/pages/posts/navigation/DomainSelector.vue (1)
23-24: LGTM!Also applies to: 89-93, 143-147
components/pages/posts/post/PostComponent.vue (1)
15-32: LGTM!Also applies to: 69-75, 91-92, 175-176, 180-282
package.json (1)
5-5: LGTM!Also applies to: 9-10
pages/premium/additional-boorus.vue (1)
4-4: LGTM!Also applies to: 11-12, 181-189, 206-209, 213-215, 234-237, 243-243, 245-246, 266-269, 285-288, 301-304, 314-318, 421-425
test/assets/router-helper.test.ts (1)
1-2: LGTM!Also applies to: 24-24, 29-29, 43-66
components/pages/posts/post/PostMedia.vue (1)
12-12: LGTM!Also applies to: 40-67, 72-76, 86-88, 92-92, 94-99, 127-127, 136-143, 145-145, 282-296, 298-319, 320-327, 329-345, 444-447, 541-541, 547-547, 550-552, 563-563, 565-565, 568-568, 574-574, 577-579, 587-587, 593-598, 616-620, 704-706
AGENTS.md (1)
8-8: LGTM!Also applies to: 14-14, 18-18, 25-33, 39-39, 68-68, 79-80, 86-88, 97-125, 154-156
nuxt.config.js (1)
9-45: LGTM!Also applies to: 92-92, 100-100, 108-108, 133-133, 141-141, 145-182, 187-187, 189-194, 210-210, 283-283, 401-401, 463-463
test/helper.ts (1)
1-1: LGTM!Also applies to: 4-17, 62-62, 65-65, 67-69
pages/tags/[domain]/[tag].vue (1)
1-127: LGTM!Also applies to: 129-175, 176-222, 231-244, 285-301
test/pages/posts.test.ts (7)
150-162: LGTM!
196-203: LGTM!
350-351: LGTM!
360-368: LGTM!
387-399: LGTM!Also applies to: 415-428
430-463: LGTM!
478-499: LGTM!
| const tagsKey = computed( | ||
| () => 'preselectedTags:' + `${props.domain}:` + tagsWithMedia.value.map((tag) => tag.media.length).join('-') | ||
| ) | ||
|
|
||
| const preselectedTags = useState<typeof props.tags>(tagsKey) | ||
| const preselectedTags = useState<typeof props.tags>(tagsKey.value) | ||
|
|
||
| callOnce(tagsKey, () => { | ||
| preselectedTags.value = props.tags.map((tag) => ({ | ||
| callOnce(tagsKey.value, () => { | ||
| preselectedTags.value = tagsWithMedia.value.map((tag) => ({ |
There was a problem hiding this comment.
Strengthen tagsKey to prevent stale featured-tag reuse.
tagsKey only uses domain + media lengths, so different tag payloads can share the same key and reuse outdated preselectedTags state.
Suggested fix
const tagsKey = computed(
- () => 'preselectedTags:' + `${props.domain}:` + tagsWithMedia.value.map((tag) => tag.media.length).join('-')
+ () =>
+ 'preselectedTags:' +
+ `${props.domain}:` +
+ tagsWithMedia.value.map((tag) => `${tag.name}:${tag.media[0]?.src ?? ''}`).join('|')
)📝 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.
| const tagsKey = computed( | |
| () => 'preselectedTags:' + `${props.domain}:` + tagsWithMedia.value.map((tag) => tag.media.length).join('-') | |
| ) | |
| const preselectedTags = useState<typeof props.tags>(tagsKey) | |
| const preselectedTags = useState<typeof props.tags>(tagsKey.value) | |
| callOnce(tagsKey, () => { | |
| preselectedTags.value = props.tags.map((tag) => ({ | |
| callOnce(tagsKey.value, () => { | |
| preselectedTags.value = tagsWithMedia.value.map((tag) => ({ | |
| const tagsKey = computed( | |
| () => | |
| 'preselectedTags:' + | |
| `${props.domain}:` + | |
| tagsWithMedia.value.map((tag) => `${tag.name}:${tag.media[0]?.src ?? ''}`).join('|') | |
| ) | |
| const preselectedTags = useState<typeof props.tags>(tagsKey.value) | |
| callOnce(tagsKey.value, () => { | |
| preselectedTags.value = tagsWithMedia.value.map((tag) => ({ |
🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 30 - 37, tagsKey is
currently derived only from props.domain and
tagsWithMedia.value.map(...).lengths which can collide when tag payloads change
but media lengths remain same, causing stale preselectedTags reuse; update
tagsKey (used when creating preselectedTags via useState and callOnce) to
incorporate a stable fingerprint of the actual tag payloads (e.g., join tag ids
or names or compute a short hash from tagsWithMedia.value.map(tag =>
tag.id||tag.name||JSON.stringify(tag))) so the key changes whenever tag content
changes and prevents reuse of outdated state for preselectedTags.
| const canParseUrl = URL.canParse | ||
|
|
||
| return source.hostname | ||
| function getHostnameFromUrl(source: string) { | ||
| return canParseUrl(source) ? new URL(source).hostname : source | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Current browser support status for URL.canParse() and URL.parse() in evergreen + Safari versions, and recommended fallback pattern when unavailable.
💡 Result:
URL.canParse support (evergreen + Safari): - Safari 17 (and iOS 17) supports URL.canParse; Safari versions earlier than 17 do not. [1][2][3] URL.parse support (evergreen + Safari): - Safari: supported starting Safari 18.0 (and Safari iOS 18.0). [4][5] Recommended fallback pattern when unavailable: 1) Prefer URL.parse (non-throwing) when available; otherwise use URL.canParse + new URL, and finally fall back to try/catch. 2) Gate on feature presence ("parse" or "canParse" in URL / typeof URL.parse/canParse) because older browsers won’t have the methods. Example helper (non-throwing parse when possible): const parseMaybeUrl = (input, base) => { // URL.parse: returns URL or null (no exceptions) if ("parse" in URL && typeof URL.parse === "function") { return URL.parse(input, base); } // URL.canParse: boolean check + new URL constructor (which throws on invalid) if ("canParse" in URL && typeof URL.canParse === "function") { if (base!== undefined) { return URL.canParse(input, base)? new URL(input, base): null; } return URL.canParse(input)? new URL(input): null; } // Last resort for very old browsers: try/catch around new URL try { return base!== undefined? new URL(input, base): new URL(input); } catch { return null; } }; Notes: - MDN describes URL.parse as returning null when the base URL or the resulting URL are not parsable/valid, and as a non-throwing alternative to new URL try/catch. [5] - MDN describes URL.canParse as returning a boolean for whether a URL is parseable and valid. [2]
Citations:
- 1: https://caniuse.com/mdn-api_url_canparse_static
- 2: https://developer.mozilla.org/en-US/docs/Web/API/URL/canParse_static
- 3: https://web-platform-dx.github.io/web-features-explorer/features/url-canparse/
- 4: https://caniuse.com/mdn-api_url_parse_static
- 5: https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static
🏁 Script executed:
# Check the PostSource.vue file content around the mentioned lines
head -n 165 components/pages/posts/post/PostSource.vue | tail -n 88 | cat -nRepository: Rule-34/App
Length of output: 3063
🏁 Script executed:
# Check for browser compatibility targets in the project
find . -type f \( -name "nuxt.config.*" -o -name "package.json" -o -name ".browserslistrc" -o -name "browserslist.config.*" \) -not -path "*/node_modules/*" | head -20Repository: Rule-34/App
Length of output: 87
🏁 Script executed:
# Check nuxt.config.js for compatibility settings
cat nuxt.config.js
# Check package.json for browserslist or engine constraints
cat package.json | head -50Repository: Rule-34/App
Length of output: 13933
Add a browser-compat fallback for URL.canParse.
Line 78 directly assigns URL.canParse without checking browser support. On Safari < 17 and older browsers, this is undefined, causing canParseUrl(source) calls at line 81 and template line 87 to throw TypeError at runtime.
Replace with a guarded helper following the recommended pattern from MDN:
const canParseUrl = (input) => {
if ("parse" in URL && typeof URL.parse === "function") {
return URL.parse(input) !== null
}
if ("canParse" in URL && typeof URL.canParse === "function") {
return URL.canParse(input)
}
try {
new URL(input)
return true
} catch {
return false
}
}🤖 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 `@components/pages/posts/post/PostSource.vue` around lines 78 - 82, The direct
assignment const canParseUrl = URL.canParse is unsafe on older browsers; replace
it with a guarded helper named canParseUrl (used by getHostnameFromUrl and the
template) that checks for URL.parse and URL.canParse, falls back to trying new
URL(input) inside a try/catch, and returns a boolean—then update
getHostnameFromUrl to call this helper (keep its signature) so hostname
extraction and template calls won’t throw on browsers lacking URL.canParse.
| <button | ||
| :aria-label="$t('common.openPostSourceOptions')" | ||
| class="flex items-center rounded-md px-1.5 py-1" | ||
| disabled | ||
| tabindex="-1" | ||
| type="button" | ||
| > | ||
| <LinkIcon | ||
| aria-hidden="true" | ||
| class="text-base-content h-5 w-5" | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Avoid exposing an unusable fallback control to screen readers.
This fallback is permanently non-interactive (disabled + tabindex="-1"), but the action label makes it sound clickable. Prefer a decorative element hidden from the accessibility tree.
Suggested fix
- <button
- :aria-label="$t('common.openPostSourceOptions')"
+ <span
+ aria-hidden="true"
class="flex items-center rounded-md px-1.5 py-1"
- disabled
- tabindex="-1"
- type="button"
>
<LinkIcon
aria-hidden="true"
class="text-base-content h-5 w-5"
/>
- </button>
+ </span>🤖 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 `@components/pages/posts/post/PostSourceFallback.vue` around lines 6 - 17, The
fallback control in PostSourceFallback.vue is a permanently non-interactive
button (disabled + tabindex="-1") but still exposes an action aria-label; change
it to a purely decorative element so it is removed from the accessibility tree:
replace the <button> wrapper around LinkIcon with a non-interactive element
(e.g., <span> or <div>) and remove the aria-label and tabindex, ensuring
LinkIcon stays aria-hidden="true" (or add aria-hidden="true" to the wrapper).
This keeps the visual styling (classes like "flex items-center rounded-md px-1.5
py-1") but prevents screen readers from treating it as a clickable control.
| const props = defineProps<ShareButtonProps>() | ||
|
|
||
| const { t } = useI18n() | ||
| const { toast } = useLazyToast() |
There was a problem hiding this comment.
Handle async toast promises in clipboard paths.
After moving to useLazyToast() (Line 13), toast.success(...) and toast.error(...) at Line 40-Line 42 should be awaited (or caught) to prevent unhandled promise rejections.
Also applies to: 40-42
🤖 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 `@components/shared/ShareButton.vue` at line 13, The toast calls from
useLazyToast() can return promises and are currently fired without awaiting or
catching, causing potential unhandled rejections; update the clipboard
success/failure paths (where toast.success(...) and toast.error(...) are
invoked) to await the promises or attach .catch handlers (e.g., await
toast.success(...) or toast.success(...).catch(...)) inside the copy/clipboard
handler function so that both success and error toast calls from useLazyToast()
are properly handled.
| "premium": { | ||
| "tagCollections": "Coleções de tags", | ||
| "additionalBoorus": "Boorus adicionais", | ||
| "backup": "Backup", | ||
| "addMoreBoorus": "Adicionar mais Boorus", | ||
| "signIn": { | ||
| "seoTitle": "Entrar", | ||
| "seoDescription": "Entre no {name}.", | ||
| "pageTitle": "Entrar", | ||
| "licenseLabel": "Licença", | ||
| "forgotLicense": "Esqueceu a licença?", | ||
| "signInButton": "Entrar", | ||
| "notMember": "Não é membro Premium?", | ||
| "subscribeNow": "Assine agora" | ||
| }, | ||
| "dashboard": { | ||
| "seoTitle": "Painel Premium", | ||
| "signOutLabel": "Sair", | ||
| "pageTitle": "Painel", | ||
| "signedInAs": "Conectado como", | ||
| "thankYouTitle": "Obrigado pelo seu apoio", | ||
| "thankYouText": "Curta navegação sem anúncios, salvamento de posts e todos os recursos Premium!", | ||
| "expiredTitle": "Sua assinatura expirou", | ||
| "expiredText": "Sua conta está em modo somente leitura. Você ainda pode ver seus posts salvos, mas não pode criar novos", | ||
| "resubscribeText": "Para continuar aproveitando os recursos Premium, assine novamente", | ||
| "subscribeNow": "Assine agora", | ||
| "featuresTitle": "Recursos Premium", | ||
| "savedPostsName": "Posts salvos", | ||
| "savedPostsDescription": "Salve posts e aproveite depois", | ||
| "tagCollectionsName": "Coleções de tags", | ||
| "tagCollectionsDescription": "Crie listas de tags para pesquisar ou filtrar posts rapidamente", | ||
| "additionalBoorusName": "Boorus adicionais", | ||
| "additionalBoorusDescription": "Navegue por posts de sites adicionais", | ||
| "backupName": "Backup e restauração", | ||
| "backupDescription": "Faça backup das suas coleções de tags e configurações", | ||
| "discordRoleName": "Resgatar cargo no Discord", | ||
| "discordRoleDescription": "Conecte o Discord para receber o cargo Premium", | ||
| "feedbackTitle": "Feedback", | ||
| "feedbackText": "Tem uma sugestão ou encontrou um bug? Me avise! Seu feedback ajuda a melhorar o app para todo mundo.", | ||
| "contactSupport": "Falar com o suporte", | ||
| "manageSubscription": "Gerenciar assinatura" | ||
| }, | ||
| "backupPage": { | ||
| "seoTitle": "Backup", | ||
| "pageTitle": "Backup e restauração", | ||
| "pageDescription": "Faça backup das suas coleções de tags e configurações", | ||
| "manualNote": "Este é um processo manual. Lembre-se de fazer backup dos seus dados regularmente", | ||
| "backupButton": "Backup", | ||
| "restoreButton": "Restaurar" | ||
| }, | ||
| "tagCollectionsPage": { | ||
| "seoTitle": "Coleções de tags", | ||
| "pageTitle": "Coleções de tags", | ||
| "pageDescription": "Listas de tags para pesquisar ou filtrar posts rapidamente", | ||
| "dragToReorder": "Arraste para reordenar", | ||
| "tagsInCollection": "tag | tags", | ||
| "resetToDefault": "Redefinir para o padrão", | ||
| "createCollection": "Criar coleção", | ||
| "createTitle": "Criar coleção de tags", | ||
| "editTitle": "Editar coleção de tags", | ||
| "nameLabel": "Nome", | ||
| "tagsLabel": "Tags", | ||
| "oneTagPerLine": "Uma tag por linha", | ||
| "deleteButton": "Excluir", | ||
| "createButton": "Criar", | ||
| "saveButton": "Salvar", | ||
| "excludeTagsHint": "Use {code} para excluir tags", | ||
| "defaultAnimated": "Animado (vídeo ou GIFs)", | ||
| "defaultVideosWithSound": "Vídeos com som", | ||
| "defaultLongVideosWithSound": "Vídeos longos com som" | ||
| }, | ||
| "additionalBoorusPage": { | ||
| "seoTitle": "Boorus adicionais", | ||
| "pageTitle": "Boorus adicionais", | ||
| "pageDescription": "Adicione e edite Boorus compatíveis", | ||
| "emptyState": "Comece a adicionar Boorus!", | ||
| "defaultCannotEdit": "Boorus padrão não podem ser editados", | ||
| "dragToReorder": "Arraste para reordenar", | ||
| "resetButton": "Redefinir", | ||
| "addBooruButton": "Adicionar Booru", | ||
| "addTitle": "Adicionar Booru", | ||
| "editTitle": "Editar Booru", | ||
| "domainLabel": "Domínio", | ||
| "domainDescription": "Apenas o domínio. Não a URL completa", | ||
| "typeLabel": "Tipo", | ||
| "typeDescription": "Geralmente indicado no rodapé (parte inferior) do site do Booru", | ||
| "typeNote": "Se o tipo for igual ao domínio, escolha essa opção", | ||
| "deleteButton": "Excluir", | ||
| "addButton": "Adicionar", | ||
| "saveButton": "Salvar" | ||
| }, | ||
| "forgotPassword": { | ||
| "seoTitle": "Esqueceu a licença", | ||
| "seoDescription": "Recupere sua chave de licença do {name}.", | ||
| "pageTitle": "Esqueceu a licença?", | ||
| "pageDescription": "Digite seu e-mail e nós enviaremos para você", | ||
| "emailLabel": "E-mail", | ||
| "sendLicense": "Enviar licença", | ||
| "gotLicense": "Já recebeu sua chave de licença?", | ||
| "signIn": "@:pages.premium.signIn.pageTitle" | ||
| }, | ||
| "landingPage": { | ||
| "seoTitle": "Premium", | ||
| "signIn": "@:pages.premium.signIn.pageTitle", | ||
| "heroTitle": "Navegue sem anúncios e salve posts", | ||
| "heroSubtitle": "Foque no hentai sem distrações, salve posts para ver depois, carregamento de imagens mais rápido e muito mais!", | ||
| "getPremium": "Assinar Premium", | ||
| "lovedByCustomers": "Amado por mais de {count} clientes", | ||
| "fromSource": "de {source}", | ||
| "andManyMore": "E muito mais", | ||
| "premiumPlans": "Planos Premium", | ||
| "allPlansInclude": "Todos os planos incluem…", | ||
| "featuresDescription": "Acesso a {count} sites adicionais e {features} recursos exclusivos!", | ||
| "cancelAnytime": "Cancele quando quiser", | ||
| "safeDiscreetBilling": "Cobrança segura e discreta", | ||
| "trustedByCustomers": "Confiado por mais de {count} clientes", | ||
| "savePercent": "Economize {percent}%", | ||
| "faqTitle": "Perguntas frequentes", | ||
| "paymentDialogTitle": "Instruções de pagamento - {name}", | ||
| "paymentDialogSubtitle": "Escolha seu método de pagamento preferido", | ||
| "featureNoAds": "Sem anúncios", | ||
| "featureFasterImages": "Carregamento de imagem mais rápido", | ||
| "featureAdditionalWebsites": "Acesso a {count} sites adicionais", | ||
| "featureSavePosts": "Salve posts, sincronizados em todos os dispositivos", | ||
| "featureDownload": "Baixe posts com um clique", | ||
| "featureFindSource": "Encontre a fonte (artista) de um post", | ||
| "featureHistory": "Histórico integrado para retomar a navegação", | ||
| "featureTagCollections": "Crie coleções de tags", | ||
| "featureBlockTags": "Bloqueie tags para filtrar posts", | ||
| "featureProxy": "Proxy para contornar bloqueio de site no seu país", | ||
| "featureDiscordRole": "Cargo \"Premium\" no Discord", | ||
| "featureSupportDev": "Apoie o desenvolvimento", | ||
| "creditCard": "Cartão de crédito", | ||
| "cryptocurrency": "Criptomoeda", | ||
| "faq1Question": "Como recebo acesso depois do pagamento?", | ||
| "faq1Answer": "Após o pagamento, você receberá um e-mail com instruções para entrar na sua conta Premium.", | ||
| "faq2Question": "Não recebeu o e-mail da licença?", | ||
| "faq2Answer": "Às vezes o e-mail de confirmação pode não chegar ou cair no spam — verifique lá primeiro. Se não estiver em lugar nenhum, vá para a página \"Esqueceu a licença\" e peça para enviar novamente usando o e-mail que você usou no pagamento.", | ||
| "faq3Question": "Quais formas de pagamento são aceitas?", | ||
| "faq3Answer": "Aceitamos cartões de crédito/débito e PayPal.\nTambém aceitamos pagamentos em criptomoeda.\n\nProcure o ícone de cartão / cripto na janela de pagamento.", | ||
| "faq4Question": "Posso cancelar minha assinatura?", | ||
| "faq4Answer": "Sim, você pode cancelar sua assinatura a qualquer momento.\nPara assinaturas feitas por plataformas externas, cancele diretamente na plataforma.\nCompras únicas não precisam de cancelamento.", | ||
| "faq5Question": "O que acontece depois que eu cancelar?", | ||
| "faq5Answer": "Você mantém o acesso Premium até o fim do período pago.\n\nDepois que sua assinatura terminar, sua conta ficará em modo somente leitura.", | ||
| "faq6Question": "O acesso vitalício é realmente para sempre?", | ||
| "faq6Answer": "Sim! Vitalício significa acesso permanente aos recursos Premium enquanto o {name} existir.", | ||
| "subscribeWithCreditCard": "Assinar com cartão de crédito", | ||
| "subscribeWithCrypto": "Assinar com cripto", | ||
| "payWithCreditCard": "Pagar com cartão de crédito", | ||
| "payWithCrypto": "Pagar com cripto", | ||
| "planMonthlyName": "@:common.monthly", | ||
| "planMonthlyDescription": "Cobrado mensalmente", | ||
| "plan30DaysName": "Acesso de 30 dias", | ||
| "planYearlyName": "@:common.yearly", | ||
| "planYearlyDescription": "Cobrado como €59.64; Economize 4 meses", | ||
| "planLifetimeName": "Vitalício", | ||
| "oneTimePayment": "Pagamento único", | ||
| "perMonth": "mês", | ||
| "testimonial1Text": "Você pula todos os anúncios e foca no pornô", | ||
| "testimonial1From": "Discord", | ||
| "testimonial2Text": "Basicamente O mecanismo de busca de hentai para mobile", | ||
| "testimonial2From": "Feedback", | ||
| "testimonial3Text": "Baixar vídeos é ótimo, e salvar para ver depois é melhor ainda", | ||
| "testimonial3From": "Discord", | ||
| "testimonial4Text": "Os sites extras fazem diferença, descobri muitos artistas e conteúdos novos", | ||
| "testimonial4From": "Feedback" | ||
| } | ||
| }, |
There was a problem hiding this comment.
Missing pages.premium.savedPostsPage.title translation key in pt locale.
pages/premium/saved-posts/[domain].vue uses t('pages.premium.savedPostsPage.title') (Line 623), but this locale file does not define that key, so pt users may see a fallback/missing key string.
Suggested minimal patch
"backupPage": {
"seoTitle": "Backup",
"pageTitle": "Backup e restauração",
"pageDescription": "Faça backup das suas coleções de tags e configurações",
"manualNote": "Este é um processo manual. Lembre-se de fazer backup dos seus dados regularmente",
"backupButton": "Backup",
"restoreButton": "Restaurar"
},
+ "savedPostsPage": {
+ "title": "Posts salvos"
+ },
"tagCollectionsPage": {🤖 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 `@i18n/locales/pt.json` around lines 160 - 327, The pt locale is missing the
translation key pages.premium.savedPostsPage.title referenced by
pages/premium/saved-posts/[domain].vue
(t('pages.premium.savedPostsPage.title')), causing fallbacks; add the key under
the premium section in i18n/locales/pt.json (e.g., add savedPostsPage: {
"title": "..." }) with the appropriate Portuguese string, ensuring the JSON
structure matches existing nested keys (pages → premium → savedPostsPage →
title) and run a quick lint/format check to keep the file valid.
| const route = useRoute() | ||
| const localePath = useLocalePath() | ||
| const { t } = useI18n() | ||
| const { toast } = useLazyToast() |
There was a problem hiding this comment.
Await async toast calls after switching to useLazyToast().
After Line 19, toast.error(...) and toast.success(...) return promises. The calls at Line 83 and Line 96 should be awaited (or explicitly handled) to avoid unhandled promise rejections on lazy-import failure.
Also applies to: 83-83, 96-96
🤖 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 `@pages/premium/dashboard.vue` at line 19, The toast instance from
useLazyToast() returns promise-based methods; update the calls to
toast.error(...) and toast.success(...) (the usages in this file at the spots
invoking toast, e.g., around the handlers that call toast.error and
toast.success) to await the returned promises or explicitly handle them (e.g.,
await toast.error(...); or toast.error(...).catch(() => {}) ) so any lazy-import
failure is handled and avoids unhandled promise rejections; ensure the functions
calling these toasts are marked async if needed (e.g., the action/handler
functions that call toast.error/ success).
| pnpm install | ||
|
|
||
| # Serve with hot reload at localhost:8080 | ||
| npm run dev | ||
| pnpm dev | ||
|
|
||
| # Build for production | ||
| npm run build | ||
| pnpm build | ||
|
|
||
| # Generate static project | ||
| npm run generate | ||
| pnpm generate | ||
| ``` |
There was a problem hiding this comment.
Align prerequisites with the new pnpm workflow.
The setup commands now use pnpm, but the requirements section still lists NPM. Please update prerequisites to avoid contradictory setup instructions.
Suggested doc fix
- - NPM
+ - pnpm (recommended via Corepack)🤖 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 `@README.md` around lines 103 - 113, Update the README prerequisites to match
the new pnpm workflow: replace any mention of "NPM" with "pnpm", add a short
note to install pnpm (e.g., via npm: `npm install -g pnpm`) or link to pnpm
docs, and ensure the example setup commands (pnpm install, pnpm dev, pnpm build,
pnpm generate) are consistent with the prerequisites section so users aren’t
shown conflicting package manager instructions.
| function decodeImgproxySourceUrl(src: string) { | ||
| const encodedSource = src.split('/').pop() | ||
|
|
||
| if (!encodedSource) { | ||
| return null | ||
| } | ||
|
|
||
| const base64 = encodedSource.replace(/-/g, '+').replace(/_/g, '/') | ||
| const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=') | ||
|
|
||
| return Buffer.from(paddedBase64, 'base64').toString('utf8') | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Add error handling for malformed base64 input.
The Buffer.from(paddedBase64, 'base64') call will throw if the input is invalid base64, but this isn't caught. Consider wrapping the decode in try/catch to return null or a meaningful error for malformed imgproxy URLs.
🛡️ Proposed defensive fix
function decodeImgproxySourceUrl(src: string) {
const encodedSource = src.split('/').pop()
if (!encodedSource) {
return null
}
const base64 = encodedSource.replace(/-/g, '+').replace(/_/g, '/')
const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=')
+ try {
return Buffer.from(paddedBase64, 'base64').toString('utf8')
+ } catch {
+ return 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.
| function decodeImgproxySourceUrl(src: string) { | |
| const encodedSource = src.split('/').pop() | |
| if (!encodedSource) { | |
| return null | |
| } | |
| const base64 = encodedSource.replace(/-/g, '+').replace(/_/g, '/') | |
| const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=') | |
| return Buffer.from(paddedBase64, 'base64').toString('utf8') | |
| } | |
| function decodeImgproxySourceUrl(src: string) { | |
| const encodedSource = src.split('/').pop() | |
| if (!encodedSource) { | |
| return null | |
| } | |
| const base64 = encodedSource.replace(/-/g, '+').replace(/_/g, '/') | |
| const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=') | |
| try { | |
| return Buffer.from(paddedBase64, 'base64').toString('utf8') | |
| } catch { | |
| return null | |
| } | |
| } |
🤖 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/pages/posts.test.ts` around lines 6 - 17, The function
decodeImgproxySourceUrl should guard against malformed base64 causing
Buffer.from(paddedBase64, 'base64') to throw; wrap the decode step in a
try/catch inside decodeImgproxySourceUrl (catch any errors from
Buffer.from(...)) and return null (or a clear sentinel) on failure, ensuring
callers get a safe null instead of an uncaught exception.
| await page.goto(url('/es/tags/safebooru.org/1girl'), { waitUntil: 'domcontentloaded' }) | ||
|
|
||
| const canonicalHref = await page.locator('link[rel="canonical"]').getAttribute('href') | ||
| expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl') |
There was a problem hiding this comment.
Guard against null before creating URL.
The canonicalHref can be null (from getAttribute), but the code uses a non-null assertion operator. Add an explicit null check or use optional chaining with a fallback.
🛡️ Proposed fix
- expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl')
+ expect(canonicalHref).toBeTruthy()
+ expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl')Or use a more defensive approach:
- expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl')
+ const url = canonicalHref ? new URL(canonicalHref) : null
+ expect(url?.pathname).toBe('/es/tags/safebooru.org/1girl')📝 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.
| expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl') | |
| expect(canonicalHref).toBeTruthy() | |
| expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl') |
🤖 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/pages/posts.test.ts` at line 506, The test constructs new
URL(canonicalHref!) without guarding against canonicalHref being null (it comes
from element.getAttribute), so update the test to check canonicalHref is not
null before creating a URL (e.g., assert/call
expect(canonicalHref).not.toBeNull() or return early) or use optional
chaining/fallback when building the URL; ensure you reference the canonicalHref
variable and the URL construction in the assertion (expect(new
URL(...).pathname).toBe(...)) so the test no longer uses the non-null assertion
operator.
efab159 to
2af6b62
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/pages/home/PageHistory.vue (1)
33-67: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winReplace constructor
try/catchURL parsing withURL.canParse()guard for guideline compliance.Line 35 uses exception control flow for URL parsing. Per coding guidelines, prefer explicit validation with
URL.canParse()over constructortry/catch.Suggested fix
function historyPathToTitle(path: string) { - try { - const url = new URL(path, project.urls.production) - const lines = [`${t('common.domain')}: ${url.pathname.replace(/^\/posts\//, '')}`] + if (!URL.canParse(path, project.urls.production)) { + return `${t('common.domain')}: ${path}` + } + + const url = new URL(path, project.urls.production) + const lines = [`${t('common.domain')}: ${url.pathname.replace(/^\/posts\//, '')}`] const page = url.searchParams.get('page') const tags = url.searchParams.get('tags') const sort = url.searchParams.get('filter[sort]') const rating = url.searchParams.get('filter[rating]') const score = url.searchParams.get('filter[score]') @@ - return lines.join('\n') - } catch (error) { - console.error('Failed to parse history path:', error) - return `${t('common.domain')}: ${path}` - } + return lines.join('\n') }🤖 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 `@components/pages/home/PageHistory.vue` around lines 33 - 67, The historyPathToTitle function currently uses a try/catch around new URL(...) to detect invalid paths; replace that exception-based flow with an explicit guard using URL.canParse() before constructing the URL: check URL.canParse(path, project.urls.production) and if false return the fallback `${t('common.domain')}: ${path}` (and optionally log), otherwise create the URL and proceed to build lines (page, tags, sort, rating, score) exactly as currently done; update references in the function to use this guard instead of the catch block so parsing errors are avoided without exceptions.
♻️ Duplicate comments (5)
components/pages/posts/post/PostSourceFallback.vue (1)
6-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReplace disabled button with a decorative element.
The fallback is permanently non-interactive (
disabled+tabindex="-1"), but thearia-labelmakes it sound actionable to screen reader users. Use a decorative element hidden from the accessibility tree instead.♿ Proposed fix
- <button - :aria-label="$t('common.openPostSourceOptions')" + <span + aria-hidden="true" class="flex items-center rounded-md px-1.5 py-1" - disabled - tabindex="-1" - type="button" > <LinkIcon aria-hidden="true" class="text-base-content h-5 w-5" /> - </button> + </span>🤖 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 `@components/pages/posts/post/PostSourceFallback.vue` around lines 6 - 17, The element is a permanently non-interactive button (disabled + tabindex="-1") but still exposes an aria-label; replace the button with a purely decorative element so it is removed from the accessibility tree: change the <button> wrapper in PostSourceFallback.vue to a non-interactive element (e.g., <span> or <div>) that preserves the existing classes, remove the aria-label and tabindex attributes, and add aria-hidden="true" on the wrapper (the inner LinkIcon can remain as-is or also get aria-hidden) so screen readers no longer treat it as actionable.Dockerfile (1)
10-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDefaulting source-map upload to
trueis fragile for non-production builds.Please default
SENTRY_UPLOAD_SOURCE_MAPStofalseand opt in only when explicitly needed; current default can fail builds when Sentry credentials are absent.Suggested fix
-ARG SENTRY_UPLOAD_SOURCE_MAPS=true +ARG SENTRY_UPLOAD_SOURCE_MAPS=falseAs per coding guidelines: "Set
SENTRY_UPLOAD_SOURCE_MAPS=falseto skip source map uploads if needed."🤖 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 `@Dockerfile` around lines 10 - 16, The Dockerfile currently defaults SENTRY_UPLOAD_SOURCE_MAPS to true which causes failures when Sentry creds are missing; change the ARG declaration and the ENV default so SENTRY_UPLOAD_SOURCE_MAPS defaults to false (update the ARG SENTRY_UPLOAD_SOURCE_MAPS and the ENV entry SENTRY_UPLOAD_SOURCE_MAPS=${SENTRY_UPLOAD_SOURCE_MAPS}) so source-map uploads are opt-in and only occur when the build explicitly sets SENTRY_UPLOAD_SOURCE_MAPS=true.components/layout/navigation/Sidebar.vue (1)
9-13:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHydration mismatch risk in standalone mode check.
Line 10 evaluates differently on SSR vs client, so
install-appcan be rendered on SSR and removed on first client render in standalone mode.Suggested fix
+const isStandaloneDisplayMode = useState('sidebar-isStandaloneDisplayMode', () => false) + +if (import.meta.client) { + onMounted(() => { + isStandaloneDisplayMode.value = window.matchMedia('(display-mode: standalone)').matches + }) +} + const visibilityByLinkId = { - 'install-app': () => !import.meta.client || !window.matchMedia('(display-mode: standalone)').matches + 'install-app': () => !isStandaloneDisplayMode.value }🤖 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 `@components/layout/navigation/Sidebar.vue` around lines 9 - 13, The standalone-mode check causes SSR/client mismatch because it runs window.matchMedia during render; change the 'install-app' predicate in visibilityByLinkId to first check import.meta.client (e.g. () => import.meta.client && !window.matchMedia('(display-mode: standalone)').matches) so the server render treats the link as hidden and the client-only check won't remove a node after hydration; update the same predicate referenced by visibleSidebarLinks to use that guarded form.layouts/default.vue (1)
30-34:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFullscreen restore guard is still too strict.
Line 30–34 still bails when
event.targetis not anElement, so restoration can be skipped on valid exits from fullscreen.Suggested fix
- const target = event.target - - if (!(target instanceof Element)) { + const target = document.fullscreenElement ?? (event.target instanceof Element ? event.target : document.activeElement) + if (!(target instanceof Element)) { return }🤖 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 `@layouts/default.vue` around lines 30 - 34, The guard that returns when event.target is not an Element is too strict and skips valid fullscreen exits; change the condition so it only bails when a non-null target exists but is not an Element (e.g., replace "if (!(target instanceof Element)) return" with "if (target !== null && !(target instanceof Element)) return"), or remove the check entirely and rely on document.fullscreenElement handling; update the code around the target/event.target usage (the local variable target and the fullscreen restore logic) accordingly so restoration runs when event.target is null or undefined.test/pages/posts.test.ts (1)
535-535:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard
canonicalHrefbefore URL construction.On Line 535,
getAttribute('href')can returnnull;new URL(canonicalHref!)can throw before the actual assertion runs. Add a null assertion check first to keep failures diagnostic and deterministic.Proposed fix
const canonicalHref = await page.locator('link[rel="canonical"]').getAttribute('href') - expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl') + expect(canonicalHref).toBeTruthy() + expect(new URL(canonicalHref as string).pathname).toBe('/es/tags/safebooru.org/1girl')🤖 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/pages/posts.test.ts` at line 535, The test currently calls new URL(canonicalHref!) which can throw if canonicalHref is null; first assert/guard that the href retrieved from getAttribute('href') (the canonicalHref variable) is not null (e.g., expect(canonicalHref).not.toBeNull() or if (!canonicalHref) fail('missing canonical href')), then construct new URL(canonicalHref) and assert on .pathname; update the test around the canonicalHref usage in posts.test.ts to perform this null-check before creating the URL so failures are deterministic and diagnostic.
🤖 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/home/FeaturedTags.vue`:
- Around line 107-113: Add the required imgproxy provider to the NuxtImg usage:
update the <NuxtImg> component (the instance that uses :alt, :decoding,
:fetchpriority, :loading, :preload and :src from tag.media[0].src) to include
provider="imgproxy" while preserving the existing isPriorityMedia(index) logic
and the preload: { fetchPriority: 'high' } usage; ensure you only add the
provider prop and do not remove or alter the current conditional props or :src
referencing tag.media[0].src.
In `@components/pages/posts/post/PostDownloadFallback.vue`:
- Around line 6-17: Replace the permanently non-interactive <button> (with
aria-label, disabled and tabindex) with a purely decorative element so screen
readers don't treat it as actionable: remove the aria-label, disabled, tabindex
and type attributes from the component and replace the <button> wrapper with a
non-interactive container (e.g., a <span> or <div>) that retains the visual
classes ("rounded-md px-1.5 py-1") and mark it aria-hidden="true"; keep the
ArrowDownTrayIcon inside but ensure the icon also has aria-hidden="true" and no
role, so the whole fallback is removed from the accessibility tree.
In `@components/pages/posts/post/PostShareFallback.vue`:
- Around line 6-17: The button is permanently non-interactive but exposes an
action via aria-label; replace the disabled <button> wrapper with a purely
decorative element (e.g., a <span> or <div>) and remove the
aria-label/disabled/tabindex attributes so it is not announced to assistive
tech; ensure the wrapper has aria-hidden="true" and preserves the visual classes
(rounded-md px-1.5 py-1) and keep the inner ShareIcon (which already has
aria-hidden="true") for visual display only.
In `@composables/useDialogManagerState.ts`:
- Around line 7-13: isStandaloneDisplayMode is initialized to false and only
updated inside onMounted, causing initial computed consumers to see the wrong
value; change the initialization so when running in the client
(import.meta.client) you synchronously set the initial value from
window.matchMedia('(display-mode: standalone)').matches (e.g.
useState('isStandaloneDisplayMode', () => import.meta.client ?
window.matchMedia(...).matches : false) or perform an immediate client-side
assignment right after useState), keep the onMounted listener for future
updates, and apply the same pattern for the other similar state at the other
location (the one referenced in the comment).
In `@composables/useLazyToast.ts`:
- Around line 31-50: The async wrapper methods on the toast object (toast.error,
toast.info, toast.success, toast.warning) can propagate rejected promises from
loadToast(shouldRenderToaster) to callers that do not await them; wrap the
loadToast call and subsequent toast.* invocation in a try/catch (or attach a
.catch) inside each wrapper to handle and swallow/log errors so no rejection
escapes to fire-and-forget call sites, and ensure the methods return a resolved
value (or undefined) on failure; update the implementations in useLazyToast.ts
to use loadToast/shouldRenderToaster within local try/catch blocks and call
processLogger.warn or console.warn with context (e.g., "toast load failed") when
catching errors.
In `@pages/index.vue`:
- Around line 667-670: The search action target in the potentialAction array is
hardcoded to "/posts/..." causing non-default locales to point to the default
route; update the defineSearchAction call in pages/index.vue to build the target
using the current locale (e.g., prefixing the path with the active locale or
using the router/i18n locale value) so it becomes
"/{locale}/posts/rule34.xxx?tags={search_term_string}" (ensure fallback for
default locale keeps the same path), referencing the potentialAction array and
defineSearchAction to locate where to change the target construction.
In `@pages/tags/`[domain]/[tag].vue:
- Around line 194-203: The redirect from pages/tags/[domain]/[tag].vue uses
navigateTo and adds query param source_booru but the tags page never reads that
query to trigger the premium upsell; update the tags page to behave like
pages/posts/[domain].vue by detecting the source_booru (and/or the existing
utm_* params) from route.query on mount and invoking the same premium upsell
reopening logic (the function or composable used in posts page to open the
premium prompt), or alternatively preserve and rename the query to the exact key
posts page expects; specifically, locate the navigateTo call and the query param
source_booru and ensure the receiving component (pages/tags/[domain]/[tag].vue)
reads route.query.source_booru and calls the same opener function used by
pages/posts/[domain].vue.
---
Outside diff comments:
In `@components/pages/home/PageHistory.vue`:
- Around line 33-67: The historyPathToTitle function currently uses a try/catch
around new URL(...) to detect invalid paths; replace that exception-based flow
with an explicit guard using URL.canParse() before constructing the URL: check
URL.canParse(path, project.urls.production) and if false return the fallback
`${t('common.domain')}: ${path}` (and optionally log), otherwise create the URL
and proceed to build lines (page, tags, sort, rating, score) exactly as
currently done; update references in the function to use this guard instead of
the catch block so parsing errors are avoided without exceptions.
---
Duplicate comments:
In `@components/layout/navigation/Sidebar.vue`:
- Around line 9-13: The standalone-mode check causes SSR/client mismatch because
it runs window.matchMedia during render; change the 'install-app' predicate in
visibilityByLinkId to first check import.meta.client (e.g. () =>
import.meta.client && !window.matchMedia('(display-mode: standalone)').matches)
so the server render treats the link as hidden and the client-only check won't
remove a node after hydration; update the same predicate referenced by
visibleSidebarLinks to use that guarded form.
In `@components/pages/posts/post/PostSourceFallback.vue`:
- Around line 6-17: The element is a permanently non-interactive button
(disabled + tabindex="-1") but still exposes an aria-label; replace the button
with a purely decorative element so it is removed from the accessibility tree:
change the <button> wrapper in PostSourceFallback.vue to a non-interactive
element (e.g., <span> or <div>) that preserves the existing classes, remove the
aria-label and tabindex attributes, and add aria-hidden="true" on the wrapper
(the inner LinkIcon can remain as-is or also get aria-hidden) so screen readers
no longer treat it as actionable.
In `@Dockerfile`:
- Around line 10-16: The Dockerfile currently defaults SENTRY_UPLOAD_SOURCE_MAPS
to true which causes failures when Sentry creds are missing; change the ARG
declaration and the ENV default so SENTRY_UPLOAD_SOURCE_MAPS defaults to false
(update the ARG SENTRY_UPLOAD_SOURCE_MAPS and the ENV entry
SENTRY_UPLOAD_SOURCE_MAPS=${SENTRY_UPLOAD_SOURCE_MAPS}) so source-map uploads
are opt-in and only occur when the build explicitly sets
SENTRY_UPLOAD_SOURCE_MAPS=true.
In `@layouts/default.vue`:
- Around line 30-34: The guard that returns when event.target is not an Element
is too strict and skips valid fullscreen exits; change the condition so it only
bails when a non-null target exists but is not an Element (e.g., replace "if
(!(target instanceof Element)) return" with "if (target !== null && !(target
instanceof Element)) return"), or remove the check entirely and rely on
document.fullscreenElement handling; update the code around the
target/event.target usage (the local variable target and the fullscreen restore
logic) accordingly so restoration runs when event.target is null or undefined.
In `@test/pages/posts.test.ts`:
- Line 535: The test currently calls new URL(canonicalHref!) which can throw if
canonicalHref is null; first assert/guard that the href retrieved from
getAttribute('href') (the canonicalHref variable) is not null (e.g.,
expect(canonicalHref).not.toBeNull() or if (!canonicalHref) fail('missing
canonical href')), then construct new URL(canonicalHref) and assert on
.pathname; update the test around the canonicalHref usage in posts.test.ts to
perform this null-check before creating the URL so failures are deterministic
and diagnostic.
🪄 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: 74a73a49-32e2-4f82-bc86-9b86749c3212
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (85)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useAdvertisements.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/pt.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/server/path.test.ts
💤 Files with no reviewable changes (4)
- test/pages/premium/backup.test.ts
- app.vue
- server/plugins/lcp-preload-fetchpriority.ts
- app/router.options.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,js,jsx,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use camelCase for variable names and follow Prettier formatting: 120-char print width, no semicolons, single quotes, trailing commas removed, single attribute per line in Vue templates.
For URL validation/parsing, prefer
URL.canParse()orURL.parse()over constructortry/catch. UseURL.parse()when the parsed URL object is needed with fallback toURL.canParse()in browser code if compatibility matters.
Files:
composables/navigation/useMenu.tscomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/layout/modal/ReviewPrompt.vueplugins/040.matomo.client.tscomposables/useFaviconUrl.tscomponents/pages/home/Newsletter.vuepages/dmca.vuecomponents/pages/home/PageHistorySection.vueconfig/i18n.tstest/helper.tstest/server/path.test.tscomponents/pages/posts/post/PostTagsToggleFallback.vuecomposables/useDialogManagerState.tstest/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tscomposables/useAdvertisements.tsassets/js/nuxt-image/imgproxy.provider.tscomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/post/PostTag.vuepages/terms-of-service.vuecomponents/shared/ContentContainer.vuetest/server-mocks/plugin.tspages/premium/sign-in.vuecomponents/layout/navigation/Sidebar.vueplugins/050.formbricks.client.tscomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuepages/settings.vuecomponents/pages/posts/post/PostComponent.vueserver/api/_sitemap-urls.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomponents/pages/posts/post/PostSource.vuepages/premium/index.vuepages/privacy-policy.vuecomponents/shared/ShareButton.vueassets/js/sidebarLinks.tscomponents/pages/home/FeaturedTags.vuepages/legal.vuepages/premium/dashboard.vuepages/other-sites.vueplugins/035.sentry.client.tspages/premium/backup.vuepages/premium/forgot-password.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/DialogManager.vuelayouts/default.vuepages/premium/tag-collections.vueassets/js/RouterHelper.tspages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomposables/useLazyToast.tscomponents/pages/posts/post/PostMedia.vuepages/posts/[domain].vuecomponents/pages/posts/navigation/DomainSelector.vuepages/tags/[domain]/[tag].vuepages/index.vuenuxt.config.jspages/cookie-policy.vuetest/pages/posts.test.ts
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Components are auto-imported without path prefix. Import them as
<DomainSelector>not<Input/DomainSelector>.Use the custom
imgproxyprovider for<NuxtImg>instead of direct image URLs. Use module API for image preload priority with@nuxt/imagev2preload: { fetchPriority: 'high' }.
Files:
components/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/post/PostTag.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostSource.vuecomponents/shared/ShareButton.vuecomponents/pages/home/FeaturedTags.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/DialogManager.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/navigation/DomainSelector.vue
plugins/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Plugins are loaded in order by numeric prefix (
020.,030.,035.,040.,050.) fromplugins/directory.
Files:
plugins/040.matomo.client.tsplugins/050.formbricks.client.tsplugins/035.sentry.client.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Page-specific SEO tags (title, description) should use
useSeoMetain the page component.Query filters use flat bracket keys (
filter[sort],filter[rating], etc.) with Vue Router's default query handling. Do not re-addqsfor nestedroute.query.filterobjects unless the URL contract changes.Keep
@formkit/auto-animateroute-scoped unless used broadly. Use localvAutoAnimateimports on pages to save bundle size.
Files:
pages/dmca.vuepages/terms-of-service.vuepages/premium/sign-in.vuepages/settings.vuepages/premium/index.vuepages/privacy-policy.vuepages/legal.vuepages/premium/dashboard.vuepages/other-sites.vuepages/premium/backup.vuepages/premium/forgot-password.vuepages/premium/tag-collections.vuepages/premium/saved-posts/[domain].vuepages/premium/additional-boorus.vuepages/posts/[domain].vuepages/tags/[domain]/[tag].vuepages/index.vuepages/cookie-policy.vue
{config/i18n.ts,nuxt.config.ts,nuxt.config.js}
📄 CodeRabbit inference engine (AGENTS.md)
Define locales in
config/i18n.tsas the single source of truth. Non-default locales (ru, es, ja) get URL prefixes with mirrored route rules for caching/SSR.
Files:
config/i18n.tsnuxt.config.js
i18n/locales/*.json
📄 CodeRabbit inference engine (AGENTS.md)
i18n JSON locale files are stored in
i18n/locales/directory for supported languages (en, ru, es, ja).
Files:
i18n/locales/ru.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/en.jsoni18n/locales/pt.jsoni18n/locales/de.json
test/**/*.{test,spec}.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Tests use
@nuxt/test-utilswith Playwright insidedescribeblocks that callawait setup({ browser: true }). Mock server-side API calls via test-only Nitro plugin attest/server-mocks/plugin.ts.Import
debugBrowserOptionsfromtest/helper.tsfor headful Playwright playback with slowMo in tests.
Files:
test/server/path.test.tstest/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
Dockerfile
📄 CodeRabbit inference engine (AGENTS.md)
Docker production multi-stage build: build stage uses
pnpm install --frozen-lockfile. SetSENTRY_UPLOAD_SOURCE_MAPS=falseto skip source map uploads if needed. Production stage copies only.output/(nonode_modules).Use
NITRO_PRESETbuild arg to select the deployment target in Docker production build.
Files:
Dockerfile
server/api/**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Server API routes use Nitro file-based routing in
server/api/directory.
Files:
server/api/_sitemap-urls.ts
{app.vue,nuxt.config.ts,nuxt.config.js}
📄 CodeRabbit inference engine (AGENTS.md)
Place static global SEO tags (favicon, rating, monetization, color-scheme) in
nuxt.config.jshead.meta. Place dynamic global tags that need request host inapp.vueusinguseSeoMetainsideif (import.meta.server)guard.
Files:
nuxt.config.js
{nuxt.config.ts,nuxt.config.js}
📄 CodeRabbit inference engine (AGENTS.md)
Keep
features.inlineStyles: falsein Nuxt config unless new measurements justify revisiting it. Enabling it increases HTML size significantly without improving performance score.In test mode,
$test.runtimeConfig.public.apiUrlis set to''so$fetch(baseURL: '')routes to local Nitro test server. Sentry is disabled 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-20T20:57:53.564Z
Learning: Schema.org breadcrumb item URLs should stay local/locale-relative. Do not convert breadcrumb items to `project.urls.production`; production-absolute URLs are for canonicals only.
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:57:53.564Z
Learning: Prefer high-impact, measurable optimizations over small rewrites. Keep battle-tested dependencies unless replacing one has clear, measured payoff. Verify performance changes with production build, tests, request traces, and Lighthouse before deciding to keep changes.
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:57:53.564Z
Learning: Do not add app-level HTML compression. Production is behind Cloudflare which Brotli-compresses HTML responses.
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:57:53.564Z
Learning: Keep the global TanStack Vue Query plugin unless larger measured payoff appears. Do not switch to route-scoped `QueryClient` without performance verification.
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T20:57:53.564Z
Learning: Do not add service worker logic. The service worker is intentionally disabled (`selfDestroying: true`).
🔇 Additional comments (66)
.vscode/settings.json (1)
1-3: LGTM!composables/navigation/useMenu.ts (1)
1-11: LGTM!pnpm-workspace.yaml (1)
1-6: LGTM!components/layout/ClientToaster.vue (1)
1-13: LGTM!components/layout/modal/ReviewPrompt.vue (1)
44-50: LGTM!plugins/040.matomo.client.ts (1)
2-2: LGTM!Also applies to: 41-41, 47-47
composables/useFaviconUrl.ts (1)
1-7: LGTM!components/pages/home/Newsletter.vue (1)
45-45: LGTM!pages/dmca.vue (1)
5-5: LGTM!Also applies to: 12-21, 50-52
public/llms.txt (1)
1-28: LGTM!components/pages/home/PageHistorySection.vue (1)
1-16: LGTM!config/i18n.ts (1)
9-12: LGTM!test/helper.ts (1)
1-1: LGTM!Also applies to: 4-17, 62-69
README.md (1)
103-103: LGTM!Also applies to: 106-106, 109-109, 112-112
i18n/locales/ru.json (1)
389-390: LGTM!Also applies to: 404-404
test/server/path.test.ts (1)
4-4: LGTM!components/pages/posts/post/PostTagsToggleFallback.vue (1)
1-19: LGTM!test/server/fix-canonical-queries.test.ts (1)
24-24: LGTM!Also applies to: 33-33, 36-36, 57-57
test/assets/router-helper.test.ts (1)
1-2: LGTM!Also applies to: 24-24, 29-29, 43-66
composables/useAdvertisements.ts (1)
74-82: LGTM!assets/js/nuxt-image/imgproxy.provider.ts (1)
41-43: LGTM!Also applies to: 79-87
components/pages/posts/post/PostSaveFallback.vue (1)
8-10: LGTM!Also applies to: 13-16
components/pages/posts/PostsPageFooter.vue (1)
2-8: LGTM!Also applies to: 125-125
i18n/locales/es.json (1)
389-390: LGTM!Also applies to: 404-404
components/pages/posts/post/PostTag.vue (1)
2-15: LGTM!Also applies to: 31-31
pages/terms-of-service.vue (1)
5-5: LGTM!Also applies to: 12-20
components/shared/ContentContainer.vue (1)
2-2: LGTM!Also applies to: 26-26, 35-35, 48-56
test/server-mocks/plugin.ts (1)
28-30: LGTM!Also applies to: 110-138
pages/premium/sign-in.vue (1)
8-8: LGTM!Also applies to: 61-69
plugins/050.formbricks.client.ts (1)
2-2: LGTM!Also applies to: 25-25, 31-33
i18n/locales/fr.json (1)
1-540: LGTM!components/pages/posts/navigation/DomainSelectorFallback.vue (1)
15-15: LGTM!Also applies to: 27-27, 29-30
i18n/locales/ja.json (1)
389-390: LGTM!Also applies to: 404-404
components/pages/posts/post/PostSource.vue (2)
78-82: GuardURL.canParseassignment for browser compatibility (Line 78).This is already reported in prior review and still applies.
13-13: LGTM!Also applies to: 164-164, 173-173, 175-176, 207-207, 209-210, 232-232, 234-235
components/pages/posts/post/PostChatWithAiFallback.vue (1)
2-3: LGTM!Also applies to: 6-20
pages/settings.vue (1)
9-10: LGTM!Also applies to: 32-39
i18n/locales/en.json (1)
389-390: LGTM!Also applies to: 404-404
components/pages/posts/post/PostComponent.vue (1)
2-5: LGTM!Also applies to: 7-13, 15-32, 69-75, 91-91, 175-175, 180-282
server/api/_sitemap-urls.ts (1)
9-11: LGTM!Also applies to: 17-17, 22-22
composables/useIdleTask.ts (1)
1-16: LGTM!composables/useInteractionDetector.ts (1)
1-1: LGTM!Also applies to: 10-10, 16-18
components/shared/ShareButton.vue (1)
13-13:useLazyToast()migration still leaves unhandled async toast calls.At Line 13, the switch to lazy toast still leaves
toast.success(...)andtoast.error(...)fire-and-forget incopyToClipboard(Line 40 and Line 42).assets/js/sidebarLinks.ts (1)
35-38: FAQ URL still hardcodes the host.At Line 37, the FAQ entry still uses a fixed domain instead of building from
project.urls.production.components/pages/home/FeaturedTags.vue (1)
30-32:tagsKeyfingerprint is still collision-prone.At Line 31, keying by only media lengths can reuse stale preselected state for different tag payloads.
i18n/locales/pt.json (1)
160-327:pages.premium.savedPostsPage.titleis still missing in pt locale.The
premiumsection still does not definesavedPostsPage.title.pages/premium/dashboard.vue (2)
19-19:useLazyToast()switch still leaves toast calls unhandled.At Line 19, toast is lazy-loaded, but
toast.error(...)andtoast.success(...)are still called without await/catch in the handlers.
129-137: LGTM!Also applies to: 219-221, 262-264, 281-283
pages/premium/index.vue (1)
177-181: LGTM!Also applies to: 202-207, 666-669
pages/privacy-policy.vue (1)
12-20: LGTM!pages/legal.vue (1)
12-21: LGTM!plugins/035.sentry.client.ts (1)
46-84: Initialization retry lockout is still present.A failed init still leaves the one-shot sentinel set, so the client can remain uninitialized for the rest of the session after a transient failure.
pages/other-sites.vue (1)
5-5: LGTM!Also applies to: 24-31
pages/premium/backup.vue (1)
9-9: LGTM!Also applies to: 63-64, 71-80, 127-127
pages/premium/forgot-password.vue (1)
5-5: LGTM!Also applies to: 40-49
i18n/locales/de.json (1)
1-539: LGTM!components/layout/navigation/SidebarWrapper.vue (1)
3-13: LGTM!Also applies to: 17-109
components/layout/navigation/Navbar.vue (1)
2-31: LGTM!components/layout/DialogManager.vue (1)
2-77: LGTM!Also applies to: 88-88, 100-100, 116-116, 120-120
layouts/default.vue (1)
2-6: LGTM!Also applies to: 42-42, 91-91, 93-93, 96-96
pages/premium/tag-collections.vue (1)
3-3: LGTM!Also applies to: 10-12, 147-155, 185-188, 197-199, 219-222, 237-240, 253-256, 266-270, 346-350
assets/js/RouterHelper.ts (1)
2-2: LGTM!Also applies to: 11-11, 32-32, 38-46, 62-70
pages/premium/saved-posts/[domain].vue (1)
13-13: LGTM!Also applies to: 22-22, 33-38, 91-91, 97-97, 114-117, 232-232, 618-626
pages/premium/additional-boorus.vue (1)
4-4: LGTM!Also applies to: 11-12, 181-189, 206-209, 213-215, 234-237, 243-243, 245-246, 266-269, 285-288, 301-304, 314-318, 421-425
components/pages/posts/navigation/search/TagCollections.vue (1)
17-17: LGTM!Also applies to: 86-89, 114-115, 118-121, 136-139
AGENTS.md (1)
8-8: LGTM!Also applies to: 14-14, 18-18, 25-33, 39-39, 68-68, 79-80, 86-88, 97-125, 154-156
| <NuxtImg | ||
| :alt="t('common.featuredTag', { name: tag.name })" | ||
| :decoding="index <= 4 ? undefined : 'async'" | ||
| :loading="index <= 4 ? undefined : 'lazy'" | ||
| :preload="index <= 4" | ||
| :decoding="isPriorityMedia(index) ? undefined : 'async'" | ||
| :fetchpriority="isPriorityMedia(index) ? undefined : 'low'" | ||
| :loading="isPriorityMedia(index) ? 'eager' : 'lazy'" | ||
| :preload="isPriorityMedia(index) ? { fetchPriority: 'high' } : false" | ||
| :src="tag.media[0].src" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use imgproxy provider for <NuxtImg> in this component.
Line 107 uses <NuxtImg> without the project’s required provider. Please set provider="imgproxy" (and keep the existing priority preload logic).
As per coding guidelines: "components/**/*.vue: Use the custom imgproxy provider for <NuxtImg> instead of direct image URLs. Use module API for image preload priority with @nuxt/image v2 preload: { fetchPriority: 'high' }."
🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 107 - 113, Add the
required imgproxy provider to the NuxtImg usage: update the <NuxtImg> component
(the instance that uses :alt, :decoding, :fetchpriority, :loading, :preload and
:src from tag.media[0].src) to include provider="imgproxy" while preserving the
existing isPriorityMedia(index) logic and the preload: { fetchPriority: 'high' }
usage; ensure you only add the provider prop and do not remove or alter the
current conditional props or :src referencing tag.media[0].src.
| <button | ||
| :aria-label="$t('common.downloadPost')" | ||
| class="rounded-md px-1.5 py-1" | ||
| disabled | ||
| tabindex="-1" | ||
| type="button" | ||
| > | ||
| <ArrowDownTrayIcon | ||
| aria-hidden="true" | ||
| class="text-base-content h-5 w-5" | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Replace disabled button with a decorative element.
The fallback is permanently non-interactive (disabled + tabindex="-1"), but the aria-label makes it sound actionable to screen reader users. Use a decorative element hidden from the accessibility tree instead.
♿ Proposed fix
- <button
- :aria-label="$t('common.downloadPost')"
+ <span
+ aria-hidden="true"
class="rounded-md px-1.5 py-1"
- disabled
- tabindex="-1"
- type="button"
>
<ArrowDownTrayIcon
aria-hidden="true"
class="text-base-content h-5 w-5"
/>
- </button>
+ </span>📝 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.
| <button | |
| :aria-label="$t('common.downloadPost')" | |
| class="rounded-md px-1.5 py-1" | |
| disabled | |
| tabindex="-1" | |
| type="button" | |
| > | |
| <ArrowDownTrayIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| </button> | |
| <span | |
| aria-hidden="true" | |
| class="rounded-md px-1.5 py-1" | |
| > | |
| <ArrowDownTrayIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| </span> |
🤖 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 `@components/pages/posts/post/PostDownloadFallback.vue` around lines 6 - 17,
Replace the permanently non-interactive <button> (with aria-label, disabled and
tabindex) with a purely decorative element so screen readers don't treat it as
actionable: remove the aria-label, disabled, tabindex and type attributes from
the component and replace the <button> wrapper with a non-interactive container
(e.g., a <span> or <div>) that retains the visual classes ("rounded-md px-1.5
py-1") and mark it aria-hidden="true"; keep the ArrowDownTrayIcon inside but
ensure the icon also has aria-hidden="true" and no role, so the whole fallback
is removed from the accessibility tree.
| <button | ||
| :aria-label="$t('common.sharePage')" | ||
| class="rounded-md px-1.5 py-1" | ||
| disabled | ||
| tabindex="-1" | ||
| type="button" | ||
| > | ||
| <ShareIcon | ||
| aria-hidden="true" | ||
| class="text-base-content h-5 w-5" | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
Replace disabled button with a decorative element.
The fallback is permanently non-interactive (disabled + tabindex="-1"), but the aria-label makes it sound actionable to screen reader users. Use a decorative element hidden from the accessibility tree instead.
♿ Proposed fix
- <button
- :aria-label="$t('common.sharePage')"
+ <span
+ aria-hidden="true"
class="rounded-md px-1.5 py-1"
- disabled
- tabindex="-1"
- type="button"
>
<ShareIcon
aria-hidden="true"
class="text-base-content h-5 w-5"
/>
- </button>
+ </span>📝 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.
| <button | |
| :aria-label="$t('common.sharePage')" | |
| class="rounded-md px-1.5 py-1" | |
| disabled | |
| tabindex="-1" | |
| type="button" | |
| > | |
| <ShareIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| </button> | |
| <span | |
| aria-hidden="true" | |
| class="rounded-md px-1.5 py-1" | |
| > | |
| <ShareIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| </span> |
🤖 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 `@components/pages/posts/post/PostShareFallback.vue` around lines 6 - 17, The
button is permanently non-interactive but exposes an action via aria-label;
replace the disabled <button> wrapper with a purely decorative element (e.g., a
<span> or <div>) and remove the aria-label/disabled/tabindex attributes so it is
not announced to assistive tech; ensure the wrapper has aria-hidden="true" and
preserves the visual classes (rounded-md px-1.5 py-1) and keep the inner
ShareIcon (which already has aria-hidden="true") for visual display only.
| const isStandaloneDisplayMode = useState('isStandaloneDisplayMode', () => false) | ||
|
|
||
| if (import.meta.client) { | ||
| onMounted(() => { | ||
| isStandaloneDisplayMode.value = window.matchMedia('(display-mode: standalone)').matches | ||
| }) | ||
| } |
There was a problem hiding this comment.
Initialize standalone mode before first computed evaluation.
isStandaloneDisplayMode defaults to false until onMounted, so standalone users can be incorrectly eligible for installPwa during first render.
Suggested fix
- const isStandaloneDisplayMode = useState('isStandaloneDisplayMode', () => false)
-
- if (import.meta.client) {
- onMounted(() => {
- isStandaloneDisplayMode.value = window.matchMedia('(display-mode: standalone)').matches
- })
- }
+ const isStandaloneDisplayMode = useState(
+ 'isStandaloneDisplayMode',
+ () => import.meta.client && window.matchMedia('(display-mode: standalone)').matches
+ )Also applies to: 20-22
🤖 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 `@composables/useDialogManagerState.ts` around lines 7 - 13,
isStandaloneDisplayMode is initialized to false and only updated inside
onMounted, causing initial computed consumers to see the wrong value; change the
initialization so when running in the client (import.meta.client) you
synchronously set the initial value from window.matchMedia('(display-mode:
standalone)').matches (e.g. useState('isStandaloneDisplayMode', () =>
import.meta.client ? window.matchMedia(...).matches : false) or perform an
immediate client-side assignment right after useState), keep the onMounted
listener for future updates, and apply the same pattern for the other similar
state at the other location (the one referenced in the comment).
| toast: { | ||
| async error(...args: Parameters<Toast['error']>) { | ||
| const toast = await loadToast(shouldRenderToaster) | ||
| return toast.error(...args) | ||
| }, | ||
|
|
||
| async info(...args: Parameters<Toast['info']>) { | ||
| const toast = await loadToast(shouldRenderToaster) | ||
| return toast.info(...args) | ||
| }, | ||
|
|
||
| async success(...args: Parameters<Toast['success']>) { | ||
| const toast = await loadToast(shouldRenderToaster) | ||
| return toast.success(...args) | ||
| }, | ||
|
|
||
| async warning(...args: Parameters<Toast['warning']>) { | ||
| const toast = await loadToast(shouldRenderToaster) | ||
| return toast.warning(...args) | ||
| } |
There was a problem hiding this comment.
Toast wrapper methods should not leak rejected promises to fire-and-forget call sites.
These async methods propagate import/runtime failures, and many callers don’t await toast calls. That can produce unhandledrejection noise and brittle UX on transient chunk-load failures.
Suggested fix
toast: {
async error(...args: Parameters<Toast['error']>) {
- const toast = await loadToast(shouldRenderToaster)
- return toast.error(...args)
+ try {
+ const toast = await loadToast(shouldRenderToaster)
+ return toast.error(...args)
+ } catch {
+ return undefined
+ }
},
async info(...args: Parameters<Toast['info']>) {
- const toast = await loadToast(shouldRenderToaster)
- return toast.info(...args)
+ try {
+ const toast = await loadToast(shouldRenderToaster)
+ return toast.info(...args)
+ } catch {
+ return undefined
+ }
},
async success(...args: Parameters<Toast['success']>) {
- const toast = await loadToast(shouldRenderToaster)
- return toast.success(...args)
+ try {
+ const toast = await loadToast(shouldRenderToaster)
+ return toast.success(...args)
+ } catch {
+ return undefined
+ }
},
async warning(...args: Parameters<Toast['warning']>) {
- const toast = await loadToast(shouldRenderToaster)
- return toast.warning(...args)
+ try {
+ const toast = await loadToast(shouldRenderToaster)
+ return toast.warning(...args)
+ } catch {
+ return undefined
+ }
}
}📝 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.
| toast: { | |
| async error(...args: Parameters<Toast['error']>) { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.error(...args) | |
| }, | |
| async info(...args: Parameters<Toast['info']>) { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.info(...args) | |
| }, | |
| async success(...args: Parameters<Toast['success']>) { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.success(...args) | |
| }, | |
| async warning(...args: Parameters<Toast['warning']>) { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.warning(...args) | |
| } | |
| toast: { | |
| async error(...args: Parameters<Toast['error']>) { | |
| try { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.error(...args) | |
| } catch { | |
| return undefined | |
| } | |
| }, | |
| async info(...args: Parameters<Toast['info']>) { | |
| try { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.info(...args) | |
| } catch { | |
| return undefined | |
| } | |
| }, | |
| async success(...args: Parameters<Toast['success']>) { | |
| try { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.success(...args) | |
| } catch { | |
| return undefined | |
| } | |
| }, | |
| async warning(...args: Parameters<Toast['warning']>) { | |
| try { | |
| const toast = await loadToast(shouldRenderToaster) | |
| return toast.warning(...args) | |
| } catch { | |
| return undefined | |
| } | |
| } |
🤖 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 `@composables/useLazyToast.ts` around lines 31 - 50, The async wrapper methods
on the toast object (toast.error, toast.info, toast.success, toast.warning) can
propagate rejected promises from loadToast(shouldRenderToaster) to callers that
do not await them; wrap the loadToast call and subsequent toast.* invocation in
a try/catch (or attach a .catch) inside each wrapper to handle and swallow/log
errors so no rejection escapes to fire-and-forget call sites, and ensure the
methods return a resolved value (or undefined) on failure; update the
implementations in useLazyToast.ts to use loadToast/shouldRenderToaster within
local try/catch blocks and call processLogger.warn or console.warn with context
(e.g., "toast load failed") when catching errors.
| potentialAction: [ | ||
| // TODO: Listen to router | ||
| defineSearchAction({ | ||
| target: '/?query={search_term_string}' | ||
| target: '/posts/rule34.xxx?tags={search_term_string}' |
There was a problem hiding this comment.
Make the schema search target locale-aware.
On /es, /ru, /ja, etc., this hardcoded /posts/... target points search engines to the default-locale route instead of the localized one.
💡 Minimal fix
+ const searchActionTarget = computed(() => `${localePath('/posts/rule34.xxx')}?tags={search_term_string}`)
+
useSchemaOrg([
defineBreadcrumb({
itemListElement: [
{
name: t('nav.home'),
item: localePath('/')
}
]
}),
defineWebSite({
name: project.name,
potentialAction: [
// TODO: Listen to router
defineSearchAction({
- target: '/posts/rule34.xxx?tags={search_term_string}'
+ target: searchActionTarget.value
})
]
}),📝 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.
| potentialAction: [ | |
| // TODO: Listen to router | |
| defineSearchAction({ | |
| target: '/?query={search_term_string}' | |
| target: '/posts/rule34.xxx?tags={search_term_string}' | |
| const searchActionTarget = computed(() => `${localePath('/posts/rule34.xxx')}?tags={search_term_string}`) | |
| useSchemaOrg([ | |
| defineBreadcrumb({ | |
| itemListElement: [ | |
| { | |
| name: t('nav.home'), | |
| item: localePath('/') | |
| } | |
| ] | |
| }), | |
| defineWebSite({ | |
| name: project.name, | |
| potentialAction: [ | |
| // TODO: Listen to router | |
| defineSearchAction({ | |
| target: searchActionTarget.value | |
| }) | |
| ] | |
| }), |
🤖 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 `@pages/index.vue` around lines 667 - 670, The search action target in the
potentialAction array is hardcoded to "/posts/..." causing non-default locales
to point to the default route; update the defineSearchAction call in
pages/index.vue to build the target using the current locale (e.g., prefixing
the path with the active locale or using the router/i18n locale value) so it
becomes "/{locale}/posts/rule34.xxx?tags={search_term_string}" (ensure fallback
for default locale keeps the same path), referencing the potentialAction array
and defineSearchAction to locate where to change the target construction.
2af6b62 to
84c2d22
Compare
left a comment
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
pages/index.vue (1)
153-170:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid
isPremiumshadowing in the watcher callback.Line 169 reads
isPremium.value, but the callback parameter namedisPremiumshadows the outer ref, so this guard never reflects reactive premium state inside the idle task.Suggested fix
watch( [hasInteracted, isPremium], - ([hasInteracted, isPremium]) => { + ([hasInteractedNow, isPremiumNow]) => { if (hasLoadedAds.value) { return } - if (!hasInteracted) { + if (!hasInteractedNow) { return } - if (isPremium) { + if (isPremiumNow) { return } scheduleIdleTask(async () => { if (hasLoadedAds.value || isPremium.value) { return }🤖 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 `@pages/index.vue` around lines 153 - 170, The watcher callback currently shadows the outer isPremium ref by using a parameter named isPremium, causing checks inside scheduleIdleTask (e.g., isPremium.value) to reference the wrong variable; rename the callback parameter (for example to isPremiumFlag or premiumParam) or remove it and reference the outer isPremium ref directly, then update all uses in the callback and the scheduleIdleTask guard (alongside hasInteracted and hasLoadedAds) so they access the reactive ref via isPremium.value to reflect the real premium state.
♻️ Duplicate comments (18)
components/pages/posts/post/PostSourceFallback.vue (1)
6-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAvoid exposing a non-usable fallback control to screen readers.
This button cannot be used (
disabled+tabindex="-1") but is still announced as an action. Use a decorative hidden wrapper instead.Proposed fix
- <button - :aria-label="$t('common.openPostSourceOptions')" + <span + aria-hidden="true" class="flex items-center rounded-md px-1.5 py-1" - disabled - tabindex="-1" - type="button" > <LinkIcon aria-hidden="true" class="text-base-content h-5 w-5" /> - </button> + </span>🤖 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 `@components/pages/posts/post/PostSourceFallback.vue` around lines 6 - 17, The current fallback uses a disabled, untabbable <button> (in PostSourceFallback.vue) which still exposes an actionable control to AT; replace the <button> with a purely decorative wrapper element (e.g., a <span> or <div>) that carries the same visual classes but removes interactive semantics and attributes (drop aria-label, disabled, tabindex, type) and ensure the inner LinkIcon remains aria-hidden="true" (or set the wrapper aria-hidden="true" / role="presentation") so assistive tech ignores the control.components/pages/posts/post/PostShareFallback.vue (1)
6-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse a decorative wrapper instead of an announced disabled action.
This fallback is permanently non-interactive but still exposed as an action to assistive tech. Replace the
<button>with a non-interactive element hidden from the accessibility tree.Proposed fix
- <button - :aria-label="$t('common.sharePage')" + <span + aria-hidden="true" class="rounded-md px-1.5 py-1" - disabled - tabindex="-1" - type="button" > <ShareIcon aria-hidden="true" class="text-base-content h-5 w-5" /> - </button> + </span>🤖 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 `@components/pages/posts/post/PostShareFallback.vue` around lines 6 - 17, The current <button> in PostShareFallback.vue is permanently non-interactive but still announced as an actionable control; replace it with a purely decorative wrapper (e.g., a <div> or <span>) that is removed from the accessibility tree (add aria-hidden="true" and remove tabindex/disabled/aria-label attributes) while keeping the same styling classes and rendering the ShareIcon inside so visual appearance is unchanged; ensure the wrapper does not provide any role or keyboard behavior so screen readers won't announce it as an action.composables/useFaviconUrl.ts (1)
3-4:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
URL.canParse()fallback for browser compatibility per coding guidelines.
URL.parse()is used without fallback. Per coding guidelines, browser code should useURL.parse()withURL.canParse()fallback.🔧 Proposed fix
- const url = URL.parse(normalizedUrl) + const url = URL.canParse(normalizedUrl) ? URL.parse(normalizedUrl) : null const domain = url?.hostname ?? urlOrDomain🤖 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 `@composables/useFaviconUrl.ts` around lines 3 - 4, The code calls URL.parse(normalizedUrl) directly which may not exist in all browsers; update it to first check URL.canParse(normalizedUrl) (and that URL.canParse exists) before using URL.parse, and fall back to a safe alternative when parsing isn't supported. Concretely, replace the direct URL.parse usage around the variables url / normalizedUrl / urlOrDomain with a conditional that uses URL.canParse(normalizedUrl) && URL.parse(normalizedUrl) when available, otherwise use a fallback (e.g., attempt new URL(normalizedUrl) or leave url undefined) so that domain = url?.hostname ?? urlOrDomain continues to work reliably across browsers.Dockerfile (1)
10-16:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGate source-map upload instead of defaulting it to enabled.
Defaulting
SENTRY_UPLOAD_SOURCE_MAPStotruecan break non-production builds when Sentry credentials are absent.Suggested fix
-ARG SENTRY_UPLOAD_SOURCE_MAPS=true +ARG SENTRY_UPLOAD_SOURCE_MAPS=falseBased on learnings: "Upload source maps to Sentry only in production Docker builds when
SENTRY_ORG,SENTRY_PROJECT, andSENTRY_AUTH_TOKENbuild args are provided".🤖 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 `@Dockerfile` around lines 10 - 16, Remove the unconditional default of ARG SENTRY_UPLOAD_SOURCE_MAPS=true and instead make the ENV SENTRY_UPLOAD_SOURCE_MAPS default to false so uploads are opt-in; change the Dockerfile to declare ARG SENTRY_UPLOAD_SOURCE_MAPS (no true default) and set ENV SENTRY_UPLOAD_SOURCE_MAPS=${SENTRY_UPLOAD_SOURCE_MAPS:-false} alongside the existing ENV NITRO_PRESET, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN; then ensure your production build pipeline sets the build-arg SENTRY_UPLOAD_SOURCE_MAPS=true only when NITRO_PRESET=production and all SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN build args are provided.plugins/035.sentry.client.ts (2)
45-85:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReset
initPromisewhen initialization fails so retries remain possible.If any import/init step throws,
initPromisestays set and later attempts are skipped for the session.Proposed fix
initPromise = (async () => { try { // ... } catch (error) { + initPromise = null console.error('Failed to initialize Sentry client', error) } })()🤖 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 `@plugins/035.sentry.client.ts` around lines 45 - 85, The initPromise remains set when Sentry initialization fails, preventing future retries; inside the catch block for the async IIFE that assigns initPromise, reset initPromise (e.g., set initPromise = undefined or null) before logging the error so subsequent calls can attempt initialization again, and keep referencing the same async init logic around initPromise, the Sentry.init call, and nuxtApp.hook handlers (isNuxtError, reportNuxtError, buildSentryClientInitOptions) to locate where to add the reset.
47-53:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStop importing private Sentry internals from
node_modulespaths.
import('../node_modules/@sentry/nuxt/build/module/runtime/utils.js')relies on non-public SDK internals and is upgrade-fragile.For `@sentry/nuxt` v10.x, are deep imports like "build/module/runtime/utils.js" part of the supported public API? What public alternative should be used for reporting Nuxt/Vue errors in client plugins?🤖 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 `@plugins/035.sentry.client.ts` around lines 47 - 53, Stop the deep-import: remove import('../node_modules/@sentry/nuxt/build/module/runtime/utils.js') and replace its usage with public Sentry APIs; use the Sentry object imported from '`@sentry/nuxt`' (or '`@sentry/vue`') and call Sentry.captureException / Sentry.captureEvent for reporting, and implement a small local isNuxtError/reportNuxtError shim (or import the officially exported helpers from '`@sentry/nuxt`' if they exist) instead of relying on buildSentryClientInitOptions internals—update any code that calls isNuxtError or reportNuxtError to use the local shim or direct Sentry.captureException calls, and keep buildSentryClientInitOptions only from '~/sentry.client.options'.composables/useLazyToast.ts (1)
31-50:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPrevent unhandled rejections from fire-and-forget toast calls.
These async wrapper methods still propagate failures from
loadToast(...), which can leakunhandledrejectionwhen call sites don’tawaittoasts.Proposed fix
toast: { async error(...args: Parameters<Toast['error']>) { - const toast = await loadToast(shouldRenderToaster) - return toast.error(...args) + try { + const toast = await loadToast(shouldRenderToaster) + return toast.error(...args) + } catch { + return undefined + } }, async info(...args: Parameters<Toast['info']>) { - const toast = await loadToast(shouldRenderToaster) - return toast.info(...args) + try { + const toast = await loadToast(shouldRenderToaster) + return toast.info(...args) + } catch { + return undefined + } }, async success(...args: Parameters<Toast['success']>) { - const toast = await loadToast(shouldRenderToaster) - return toast.success(...args) + try { + const toast = await loadToast(shouldRenderToaster) + return toast.success(...args) + } catch { + return undefined + } }, async warning(...args: Parameters<Toast['warning']>) { - const toast = await loadToast(shouldRenderToaster) - return toast.warning(...args) + try { + const toast = await loadToast(shouldRenderToaster) + return toast.warning(...args) + } catch { + return undefined + } } }🤖 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 `@composables/useLazyToast.ts` around lines 31 - 50, The async wrapper methods (toast.error/info/success/warning) propagate rejections from loadToast(shouldRenderToaster) which creates unhandledrejection when callers fire-and-forget; change each wrapper in useLazyToast to handle errors internally by surrounding the await loadToast(...) and subsequent toast.*(...) call with try/catch (or remove async and attach .catch()), swallowing or logging the error instead of letting it bubble up so callers need not await the toast; reference the toast.error/info/success/warning wrappers and loadToast(shouldRenderToaster) when making this change.layouts/default.vue (1)
30-34:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFullscreen exit restoration can still be skipped.
When
fullscreenchangefires with a non-Elementtarget, the handler exits early and restoration won’t run.Suggested fix
- const target = event.target - - if (!(target instanceof Element)) { + const target = + event.target instanceof Element + ? event.target + : document.activeElement instanceof Element + ? document.activeElement + : null + + if (!target) { return }Please verify by reproducing fullscreen enter/exit on media and confirming scroll restoration still triggers.
Also applies to: 42-42
🤖 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 `@layouts/default.vue` around lines 30 - 34, The fullscreenchange handler currently returns early when event.target is not an Element (the target variable and that instanceof Element check), which skips the scroll-restoration logic; change the handler so it does not return early — only gate the Element-specific operations behind `if (target instanceof Element) { ... }` and let the general restoration path (the scroll restoration / restoreScroll logic) run regardless of target type; locate the fullscreenchange handler and the `target` variable in layouts/default.vue and move or duplicate the restoration call outside the instanceof-guard so restoration always executes.components/layout/navigation/Sidebar.vue (1)
9-13:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid SSR/client divergence for
install-appvisibility.This can render the link on SSR and remove it on client standalone mode, which risks hydration mismatch.
Suggested fix
const { seasonalEmoji } = useSeasonalIcon() - const visibilityByLinkId = { - 'install-app': () => !import.meta.client || !window.matchMedia('(display-mode: standalone)').matches - } + const isStandaloneDisplayMode = useState('sidebar-isStandaloneDisplayMode', () => false) + + if (import.meta.client) { + onMounted(() => { + isStandaloneDisplayMode.value = window.matchMedia('(display-mode: standalone)').matches + }) + } - const visibleSidebarLinks = computed(() => sidebarLinks.filter((item) => visibilityByLinkId[item.id]?.() ?? true)) + const visibleSidebarLinks = computed(() => + sidebarLinks.filter((item) => !(item.id === 'install-app' && isStandaloneDisplayMode.value)) + )🤖 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 `@components/layout/navigation/Sidebar.vue` around lines 9 - 13, The install-app visibility currently uses import.meta.client and window.matchMedia which makes the server render the link but the client may remove it (hydration mismatch); update the visibilityByLinkId entry for 'install-app' so it returns a deterministic value during SSR (hide the link on server) and only checks window.matchMedia on the client, e.g. have 'install-app': () => (typeof window === 'undefined' || import.meta.env.SSR) ? false : !window.matchMedia('(display-mode: standalone)').matches; leave visibleSidebarLinks and sidebarLinks unchanged.pages/index.vue (2)
669-670:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMake the schema search target locale-aware.
Line 670 hardcodes a default-locale route; localized pages can emit mismatched structured-data targets.
Suggested fix
+ const searchActionTarget = computed(() => `${localePath('/posts/rule34.xxx')}?tags={search_term_string}`) + useSchemaOrg([ @@ defineSearchAction({ - target: '/posts/rule34.xxx?tags={search_term_string}' + target: searchActionTarget.value })🤖 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 `@pages/index.vue` around lines 669 - 670, The structured-data search action created in defineSearchAction currently hardcodes the target '/posts/rule34.xxx?tags={search_term_string}', causing mismatched targets on localized pages; update the defineSearchAction call to build the target using the current locale (e.g., prefixing with the active locale or using the router/i18n locale) so the emitted target matches localized routes — locate the defineSearchAction invocation and replace the hardcoded string with a locale-aware construction using the app's locale getter (router.currentRoute, useRoute/useI18n, or equivalent) so targets become '/{locale}/posts/...' or the correct localized path depending on the active locale.
124-136:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winCorrupted localStorage should not enable the history section.
Line 135 currently returns
trueon JSON parse failure, which makes corruptedsettings-pageHistorydata look valid.Suggested fix
try { const pageHistory = JSON.parse(rawPageHistory) return Array.isArray(pageHistory) ? pageHistory.length > 0 : Boolean(pageHistory) } catch { - return true + return false }🤖 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 `@pages/index.vue` around lines 124 - 136, The hasStoredPageHistory function currently treats a JSON.parse failure as valid by returning true; change the catch branch to return false (and optionally remove the corrupted 'settings-pageHistory' entry) so that corrupted localStorage does not enable the history section—locate hasStoredPageHistory, the JSON.parse usage for the 'settings-pageHistory' key, and update the catch to return false (or clear the key) instead of true.components/pages/home/FeaturedTags.vue (2)
30-37:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
tagsKeyis too weak and can reuse stale preselection state.Line 31 only fingerprints media lengths, so different tag payloads with matching lengths can share the same state key.
Suggested fix
const tagsKey = computed( - () => 'preselectedTags:' + `${props.domain}:` + tagsWithMedia.value.map((tag) => tag.media.length).join('-') + () => + 'preselectedTags:' + + `${props.domain}:` + + tagsWithMedia.value.map((tag) => `${tag.name}:${tag.media[0]?.src ?? ''}`).join('|') )🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 30 - 37, The computed tagsKey currently only includes media lengths and can collide for different tag payloads; update its construction (the computed named tagsKey) to incorporate a stronger fingerprint of the actual tag payload (e.g., include tag id or name and media length, or a short hash of JSON.stringify(tag) for each tag) so preselectedTags (the useState keyed by tagsKey.value) is unique per real payload; adjust code that builds tagsKey from tagsWithMedia.value (used by callOnce and preselectedTags) to use that stronger signature.
106-113:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the required imgproxy/webp/1x image config in this component.
The changed
<NuxtImg>keeps default provider behavior and omits explicitwebp+1xsettings required by project rules.As per coding guidelines: "components/**/*.vue: Generate images at 1x density only in webp format using the custom imgproxy provider to reduce bandwidth".Suggested fix
<NuxtImg + provider="imgproxy" + format="webp" + densities="1x" :alt="t('common.featuredTag', { name: tag.name })" :decoding="isPriorityMedia(index) ? undefined : 'async'" :fetchpriority="isPriorityMedia(index) ? undefined : 'low'" :loading="isPriorityMedia(index) ? 'eager' : 'lazy'" :preload="isPriorityMedia(index) ? { fetchPriority: 'high' } : false" :src="tag.media[0].src"🤖 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 `@components/pages/home/FeaturedTags.vue` around lines 106 - 113, The NuxtImg instance is using the default provider and missing the required imgproxy/webp/1x configuration; update the <NuxtImg> (the component using :src="tag.media[0].src" and isPriorityMedia(index) logic) to explicitly use the custom imgproxy provider and force webp at 1x density (for example by adding provider="imgproxy" and the provider params that set format to "webp" and device-pixel-ratio/density to 1 or equivalent), keeping the existing priority/loading props intact.i18n/locales/pt.json (1)
160-327:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd missing
pages.premium.savedPostsPage.titlein pt locale.The premium locale subtree still lacks
savedPostsPage.title, which can cause fallback text in the saved-posts premium page.Suggested fix
"backupPage": { "seoTitle": "Backup", "pageTitle": "Backup e restauração", "pageDescription": "Faça backup das suas coleções de tags e configurações", "manualNote": "Este é um processo manual. Lembre-se de fazer backup dos seus dados regularmente", "backupButton": "Backup", "restoreButton": "Restaurar" }, + "savedPostsPage": { + "title": "Posts salvos" + }, "tagCollectionsPage": {#!/bin/bash rg -n "pages\.premium\.savedPostsPage\.title" --type=vue --type=ts --type=js🤖 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 `@i18n/locales/pt.json` around lines 160 - 327, Missing translation key pages.premium.savedPostsPage.title in Portuguese causing fallback; add it under the premium subtree. Edit the pt locale object (the "premium" block) and add a savedPostsPage entry with a title property (e.g., "title": "Posts salvos") alongside any existing saved-posts related keys; ensure the key name exactly matches pages.premium.savedPostsPage.title so lookups (pages.premium.savedPostsPage.title / savedPostsPage) resolve correctly and test the saved-posts premium page to confirm no fallback appears.composables/useDialogManagerState.ts (1)
7-13:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInitialize standalone mode synchronously on client.
Line 7 defaults to
falseuntil mount, sopendingDialogcan transiently selectinstallPwafor standalone users.Suggested fix
- const isStandaloneDisplayMode = useState('isStandaloneDisplayMode', () => false) + const isStandaloneDisplayMode = useState( + 'isStandaloneDisplayMode', + () => import.meta.client && window.matchMedia('(display-mode: standalone)').matches + )🤖 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 `@composables/useDialogManagerState.ts` around lines 7 - 13, The state is initialized to false until onMounted runs, causing a transient wrong value; change the useState initializer for isStandaloneDisplayMode to compute window.matchMedia('(display-mode: standalone)').matches immediately when running on the client (e.g. useState('isStandaloneDisplayMode', () => import.meta.client ? window.matchMedia('(display-mode: standalone)').matches : false)) and remove the now-unnecessary onMounted block that sets isStandaloneDisplayMode.value; this ensures the value is correct synchronously for consumers like pendingDialog.components/pages/posts/post/PostSource.vue (1)
78-82:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard
URL.canParsewith a fallback for older browser compatibility.The direct assignment
const canParseUrl = URL.canParseis unsafe;URL.canParseis unavailable in Safari <17, Chrome <120, and other older evergreen browsers. Add a feature-detected fallback:Proposed patch
- const canParseUrl = URL.canParse + const canParseUrl = (input: string) => { + if (typeof URL.canParse === 'function') { + return URL.canParse(input) + } + + try { + new URL(input) + return true + } catch { + return false + } + } function getHostnameFromUrl(source: string) { return canParseUrl(source) ? new URL(source).hostname : source }🤖 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 `@components/pages/posts/post/PostSource.vue` around lines 78 - 82, Replace the unsafe direct assignment of URL.canParse by feature-detecting it and providing a fallback: set canParseUrl to URL.canParse bound to URL only if typeof URL.canParse === 'function', otherwise supply a fallback function that attempts to construct new URL(source) in a try/catch and returns true on success and false on error; update getHostnameFromUrl to keep using canParseUrl(source) as before so older browsers (Safari <17, Chrome <120) will use the safe fallback.test/pages/posts.test.ts (2)
6-17:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd error handling for malformed base64 input.
The
Buffer.from(paddedBase64, 'base64')call will throw if the input is invalid base64, but this isn't caught. Wrapping the decode in try/catch prevents test crashes on malformed imgproxy URLs.🛡️ Proposed defensive fix
function decodeImgproxySourceUrl(src: string) { const encodedSource = src.split('/').pop() if (!encodedSource) { return null } const base64 = encodedSource.replace(/-/g, '+').replace(/_/g, '/') const paddedBase64 = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=') + try { return Buffer.from(paddedBase64, 'base64').toString('utf8') + } catch { + return null + } }🤖 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/pages/posts.test.ts` around lines 6 - 17, The decodeImgproxySourceUrl function can throw when Buffer.from(paddedBase64, 'base64') receives malformed input; wrap the decoding step in a try/catch around the Buffer.from(...).toString('utf8') call (inside decodeImgproxySourceUrl) and return null on any error so tests don't crash on malformed imgproxy URLs, preserving the existing early-return when encodedSource is missing.
535-535:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard against null before creating URL.
The
canonicalHrefcan be null fromgetAttribute, but the code uses a non-null assertion. Add an explicit truthy check before URL construction.🛡️ Proposed fix
const canonicalHref = await page.locator('link[rel="canonical"]').getAttribute('href') + expect(canonicalHref).toBeTruthy() expect(new URL(canonicalHref!).pathname).toBe('/es/tags/safebooru.org/1girl')🤖 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/pages/posts.test.ts` at line 535, The test uses a non-null assertion on canonicalHref when constructing new URL(canonicalHref!) which can be null from getAttribute; add an explicit truthy guard before creating the URL—e.g., assert or expect canonicalHref is defined (reference canonicalHref in the test) and only then call new URL(canonicalHref) so the test fails with a clear message instead of throwing; update the assertion block around new URL(canonicalHref) in the posts test to check canonicalHref first.
🤖 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 `@assets/js/sidebarLinks.ts`:
- Line 44: The href construction using string concatenation is brittle; update
the link construction in the sidebarLinks entry (where href is set) to build the
blog URL via URL composition using the base origin (project.urls.production) and
the relative path "blog" (e.g., new URL('blog', project.urls.production)) so it
correctly handles trailing slashes and different base formats; locate the href
assignment in assets/js/sidebarLinks.ts and replace the concatenation with a
URL-built value, then convert to string for the final href.
In `@components/pages/posts/post/PostChatWithAiFallback.vue`:
- Around line 6-19: The fallback UI currently renders a non-interactive <button>
(in PostChatWithAiFallback.vue) with an aria-label and disabled/tabindex
attributes which keeps it in the accessibility tree; change it to a purely
decorative wrapper (e.g., replace the <button> with a <span aria-hidden="true">)
so assistive tech ignores it, remove aria-label/disabled/tabindex, and preserve
the existing classes and child nodes (SparklesIcon and the inner "AI" text span)
so visual styling remains unchanged.
In `@pages/posts/`[domain].vue:
- Around line 872-895: The computed firstPostsPageAsSchema assumes
post.high_res_file exists; update the firstPostsPageAsSchema computed to
defensively handle missing high_res_file by first filtering or guarding posts:
for each post check post.high_res_file (and for videos also post.preview_file)
before calling defineVideo/defineImage, using optional chaining
(post.high_res_file?.url) and sensible fallbacks (e.g., post.preview_file?.url)
or skipping posts that lack required assets; ensure you reference the symbols
firstPostsPageAsSchema, defineVideo, defineImage, and the
post.high_res_file/post.preview_file properties when implementing the guard so
schema generation never accesses undefined properties.
In `@pages/premium/backup.vue`:
- Around line 71-79: The page currently only sets a title via useSeoMeta; update
the page component to also pass a description to useSeoMeta (e.g., description:
t('pages.premium.backupPage.seoDescription')) so page-level SEO includes both
title and description; modify the call to useSeoMeta in pages/premium/backup.vue
(alongside the existing title) and ensure the translation key exists and is used
consistently with the existing t(...) calls.
In `@pages/premium/saved-posts/`[domain].vue:
- Around line 618-626: The page sets only the title but omits the description in
SEO metadata; update the component to call useSeoMeta with both title and
description (e.g., useSeoMeta({ title: t('pages.premium.savedPostsPage.title'),
description: t('pages.premium.savedPostsPage.description') })) so the page has
complete meta tags; locate the existing useSeoMeta usage near the
useSchemaOrg/defineBreadcrumb block and add the description entry using the
appropriate i18n key (t) and route/locale helpers as needed.
In `@pages/premium/tag-collections.vue`:
- Around line 147-155: The page currently calls useSeoMeta with only a title;
update the useSeoMeta call to include a description property as well (e.g.
description: t('pages.premium.tagCollectionsPage.seoDescription')) so the
component sets both title and description for SEO; locate the useSeoMeta
invocation near useSchemaOrg and add the description key using the appropriate
i18n translation string or a literal description if no translation key exists.
In `@test/pages/posts.test.ts`:
- Around line 320-333: Add a brief inline comment above the page.mouse.wheel(0,
100000) call explaining why such a large vertical delta is used (e.g., to force
a full-page scroll/pagination trigger in headless test environments where
smaller deltas may not reach the threshold), and note any alternatives or
caveats (like viewport-size dependence) so future maintainers understand the
intent; update the comment near the page.evaluate(...) block if that context is
clearer and reference the page.mouse.wheel invocation and the surrounding scroll
loop in posts.test.ts for placement.
In `@test/router.options.test.ts`:
- Around line 1-23: The tests in router.options.test.ts use plain Vitest; update
them to use `@nuxt/test-utils` browser setup by importing setup from
'`@nuxt/test-utils`' and making the describe callback async so you can call await
setup({ browser: true }) before running assertions; keep the existing route
helper and reuse routerOptions.scrollBehavior in the tests but wrap the current
test body inside the describe after the awaited setup call so the Playwright
browser environment is initialized per the repo policy.
---
Outside diff comments:
In `@pages/index.vue`:
- Around line 153-170: The watcher callback currently shadows the outer
isPremium ref by using a parameter named isPremium, causing checks inside
scheduleIdleTask (e.g., isPremium.value) to reference the wrong variable; rename
the callback parameter (for example to isPremiumFlag or premiumParam) or remove
it and reference the outer isPremium ref directly, then update all uses in the
callback and the scheduleIdleTask guard (alongside hasInteracted and
hasLoadedAds) so they access the reactive ref via isPremium.value to reflect the
real premium state.
---
Duplicate comments:
In `@components/layout/navigation/Sidebar.vue`:
- Around line 9-13: The install-app visibility currently uses import.meta.client
and window.matchMedia which makes the server render the link but the client may
remove it (hydration mismatch); update the visibilityByLinkId entry for
'install-app' so it returns a deterministic value during SSR (hide the link on
server) and only checks window.matchMedia on the client, e.g. have
'install-app': () => (typeof window === 'undefined' || import.meta.env.SSR) ?
false : !window.matchMedia('(display-mode: standalone)').matches; leave
visibleSidebarLinks and sidebarLinks unchanged.
In `@components/pages/home/FeaturedTags.vue`:
- Around line 30-37: The computed tagsKey currently only includes media lengths
and can collide for different tag payloads; update its construction (the
computed named tagsKey) to incorporate a stronger fingerprint of the actual tag
payload (e.g., include tag id or name and media length, or a short hash of
JSON.stringify(tag) for each tag) so preselectedTags (the useState keyed by
tagsKey.value) is unique per real payload; adjust code that builds tagsKey from
tagsWithMedia.value (used by callOnce and preselectedTags) to use that stronger
signature.
- Around line 106-113: The NuxtImg instance is using the default provider and
missing the required imgproxy/webp/1x configuration; update the <NuxtImg> (the
component using :src="tag.media[0].src" and isPriorityMedia(index) logic) to
explicitly use the custom imgproxy provider and force webp at 1x density (for
example by adding provider="imgproxy" and the provider params that set format to
"webp" and device-pixel-ratio/density to 1 or equivalent), keeping the existing
priority/loading props intact.
In `@components/pages/posts/post/PostShareFallback.vue`:
- Around line 6-17: The current <button> in PostShareFallback.vue is permanently
non-interactive but still announced as an actionable control; replace it with a
purely decorative wrapper (e.g., a <div> or <span>) that is removed from the
accessibility tree (add aria-hidden="true" and remove
tabindex/disabled/aria-label attributes) while keeping the same styling classes
and rendering the ShareIcon inside so visual appearance is unchanged; ensure the
wrapper does not provide any role or keyboard behavior so screen readers won't
announce it as an action.
In `@components/pages/posts/post/PostSource.vue`:
- Around line 78-82: Replace the unsafe direct assignment of URL.canParse by
feature-detecting it and providing a fallback: set canParseUrl to URL.canParse
bound to URL only if typeof URL.canParse === 'function', otherwise supply a
fallback function that attempts to construct new URL(source) in a try/catch and
returns true on success and false on error; update getHostnameFromUrl to keep
using canParseUrl(source) as before so older browsers (Safari <17, Chrome <120)
will use the safe fallback.
In `@components/pages/posts/post/PostSourceFallback.vue`:
- Around line 6-17: The current fallback uses a disabled, untabbable <button>
(in PostSourceFallback.vue) which still exposes an actionable control to AT;
replace the <button> with a purely decorative wrapper element (e.g., a <span> or
<div>) that carries the same visual classes but removes interactive semantics
and attributes (drop aria-label, disabled, tabindex, type) and ensure the inner
LinkIcon remains aria-hidden="true" (or set the wrapper aria-hidden="true" /
role="presentation") so assistive tech ignores the control.
In `@composables/useDialogManagerState.ts`:
- Around line 7-13: The state is initialized to false until onMounted runs,
causing a transient wrong value; change the useState initializer for
isStandaloneDisplayMode to compute window.matchMedia('(display-mode:
standalone)').matches immediately when running on the client (e.g.
useState('isStandaloneDisplayMode', () => import.meta.client ?
window.matchMedia('(display-mode: standalone)').matches : false)) and remove the
now-unnecessary onMounted block that sets isStandaloneDisplayMode.value; this
ensures the value is correct synchronously for consumers like pendingDialog.
In `@composables/useFaviconUrl.ts`:
- Around line 3-4: The code calls URL.parse(normalizedUrl) directly which may
not exist in all browsers; update it to first check URL.canParse(normalizedUrl)
(and that URL.canParse exists) before using URL.parse, and fall back to a safe
alternative when parsing isn't supported. Concretely, replace the direct
URL.parse usage around the variables url / normalizedUrl / urlOrDomain with a
conditional that uses URL.canParse(normalizedUrl) && URL.parse(normalizedUrl)
when available, otherwise use a fallback (e.g., attempt new URL(normalizedUrl)
or leave url undefined) so that domain = url?.hostname ?? urlOrDomain continues
to work reliably across browsers.
In `@composables/useLazyToast.ts`:
- Around line 31-50: The async wrapper methods
(toast.error/info/success/warning) propagate rejections from
loadToast(shouldRenderToaster) which creates unhandledrejection when callers
fire-and-forget; change each wrapper in useLazyToast to handle errors internally
by surrounding the await loadToast(...) and subsequent toast.*(...) call with
try/catch (or remove async and attach .catch()), swallowing or logging the error
instead of letting it bubble up so callers need not await the toast; reference
the toast.error/info/success/warning wrappers and loadToast(shouldRenderToaster)
when making this change.
In `@Dockerfile`:
- Around line 10-16: Remove the unconditional default of ARG
SENTRY_UPLOAD_SOURCE_MAPS=true and instead make the ENV
SENTRY_UPLOAD_SOURCE_MAPS default to false so uploads are opt-in; change the
Dockerfile to declare ARG SENTRY_UPLOAD_SOURCE_MAPS (no true default) and set
ENV SENTRY_UPLOAD_SOURCE_MAPS=${SENTRY_UPLOAD_SOURCE_MAPS:-false} alongside the
existing ENV NITRO_PRESET, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN; then
ensure your production build pipeline sets the build-arg
SENTRY_UPLOAD_SOURCE_MAPS=true only when NITRO_PRESET=production and all
SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN build args are provided.
In `@i18n/locales/pt.json`:
- Around line 160-327: Missing translation key
pages.premium.savedPostsPage.title in Portuguese causing fallback; add it under
the premium subtree. Edit the pt locale object (the "premium" block) and add a
savedPostsPage entry with a title property (e.g., "title": "Posts salvos")
alongside any existing saved-posts related keys; ensure the key name exactly
matches pages.premium.savedPostsPage.title so lookups
(pages.premium.savedPostsPage.title / savedPostsPage) resolve correctly and test
the saved-posts premium page to confirm no fallback appears.
In `@layouts/default.vue`:
- Around line 30-34: The fullscreenchange handler currently returns early when
event.target is not an Element (the target variable and that instanceof Element
check), which skips the scroll-restoration logic; change the handler so it does
not return early — only gate the Element-specific operations behind `if (target
instanceof Element) { ... }` and let the general restoration path (the scroll
restoration / restoreScroll logic) run regardless of target type; locate the
fullscreenchange handler and the `target` variable in layouts/default.vue and
move or duplicate the restoration call outside the instanceof-guard so
restoration always executes.
In `@pages/index.vue`:
- Around line 669-670: The structured-data search action created in
defineSearchAction currently hardcodes the target
'/posts/rule34.xxx?tags={search_term_string}', causing mismatched targets on
localized pages; update the defineSearchAction call to build the target using
the current locale (e.g., prefixing with the active locale or using the
router/i18n locale) so the emitted target matches localized routes — locate the
defineSearchAction invocation and replace the hardcoded string with a
locale-aware construction using the app's locale getter (router.currentRoute,
useRoute/useI18n, or equivalent) so targets become '/{locale}/posts/...' or the
correct localized path depending on the active locale.
- Around line 124-136: The hasStoredPageHistory function currently treats a
JSON.parse failure as valid by returning true; change the catch branch to return
false (and optionally remove the corrupted 'settings-pageHistory' entry) so that
corrupted localStorage does not enable the history section—locate
hasStoredPageHistory, the JSON.parse usage for the 'settings-pageHistory' key,
and update the catch to return false (or clear the key) instead of true.
In `@plugins/035.sentry.client.ts`:
- Around line 45-85: The initPromise remains set when Sentry initialization
fails, preventing future retries; inside the catch block for the async IIFE that
assigns initPromise, reset initPromise (e.g., set initPromise = undefined or
null) before logging the error so subsequent calls can attempt initialization
again, and keep referencing the same async init logic around initPromise, the
Sentry.init call, and nuxtApp.hook handlers (isNuxtError, reportNuxtError,
buildSentryClientInitOptions) to locate where to add the reset.
- Around line 47-53: Stop the deep-import: remove
import('../node_modules/@sentry/nuxt/build/module/runtime/utils.js') and replace
its usage with public Sentry APIs; use the Sentry object imported from
'`@sentry/nuxt`' (or '`@sentry/vue`') and call Sentry.captureException /
Sentry.captureEvent for reporting, and implement a small local
isNuxtError/reportNuxtError shim (or import the officially exported helpers from
'`@sentry/nuxt`' if they exist) instead of relying on buildSentryClientInitOptions
internals—update any code that calls isNuxtError or reportNuxtError to use the
local shim or direct Sentry.captureException calls, and keep
buildSentryClientInitOptions only from '~/sentry.client.options'.
In `@test/pages/posts.test.ts`:
- Around line 6-17: The decodeImgproxySourceUrl function can throw when
Buffer.from(paddedBase64, 'base64') receives malformed input; wrap the decoding
step in a try/catch around the Buffer.from(...).toString('utf8') call (inside
decodeImgproxySourceUrl) and return null on any error so tests don't crash on
malformed imgproxy URLs, preserving the existing early-return when encodedSource
is missing.
- Line 535: The test uses a non-null assertion on canonicalHref when
constructing new URL(canonicalHref!) which can be null from getAttribute; add an
explicit truthy guard before creating the URL—e.g., assert or expect
canonicalHref is defined (reference canonicalHref in the test) and only then
call new URL(canonicalHref) so the test fails with a clear message instead of
throwing; update the assertion block around new URL(canonicalHref) in the posts
test to check canonicalHref first.
🪄 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: 2189c4ef-933c-4ae9-9352-b6c1ecae7a3a
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (86)
.vscode/settings.jsonAGENTS.mdDockerfileREADME.mdapp.vueapp/router.options.tsassets/js/RouterHelper.tsassets/js/nuxt-image/imgproxy.provider.tsassets/js/sidebarLinks.tscomponents/layout/ClientToaster.vuecomponents/layout/DialogManager.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/layout/navigation/Navbar.vuecomponents/layout/navigation/Sidebar.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/pages/posts/post/PostComponent.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/posts/post/PostTag.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/shared/ContentContainer.vuecomponents/shared/ShareButton.vuecomposables/navigation/useMenu.tscomposables/useAdvertisements.tscomposables/useDialogManagerState.tscomposables/useFaviconUrl.tscomposables/useIdleTask.tscomposables/useInteractionDetector.tscomposables/useLazyToast.tsconfig/i18n.tsi18n/locales/de.jsoni18n/locales/en.jsoni18n/locales/es.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/pt.jsoni18n/locales/ru.jsonlayouts/default.vuenuxt.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/saved-posts/[domain].vuepages/premium/sign-in.vuepages/premium/tag-collections.vuepages/privacy-policy.vuepages/settings.vuepages/tags/[domain]/[tag].vuepages/terms-of-service.vueplugins/035.sentry.client.tsplugins/040.matomo.client.tsplugins/050.formbricks.client.tspnpm-workspace.yamlpublic/llms.txtserver/api/_sitemap-urls.tsserver/plugins/lcp-preload-fetchpriority.tstest/assets/router-helper.test.tstest/helper.tstest/pages/posts.test.tstest/pages/premium/backup.test.tstest/router.options.test.tstest/server-mocks/plugin.tstest/server/fix-canonical-queries.test.tstest/server/path.test.ts
💤 Files with no reviewable changes (3)
- server/plugins/lcp-preload-fetchpriority.ts
- app.vue
- test/pages/premium/backup.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{js,ts,vue}
📄 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/navigation/useMenu.tscomponents/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSourceFallback.vuecomposables/useAdvertisements.tstest/helper.tstest/server/path.test.tspages/legal.vuecomponents/pages/home/PageHistorySection.vueserver/api/_sitemap-urls.tscomposables/useFaviconUrl.tsconfig/i18n.tstest/server-mocks/plugin.tscomponents/pages/posts/post/PostChatWithAiFallback.vuepages/other-sites.vuecomponents/shared/ShareButton.vueplugins/050.formbricks.client.tscomponents/pages/posts/post/PostDownloadFallback.vuecomposables/useIdleTask.tscomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/home/Newsletter.vuepages/premium/forgot-password.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/post/PostTag.vuecomponents/layout/ClientToaster.vuepages/dmca.vueassets/js/sidebarLinks.tstest/router.options.test.tscomponents/pages/posts/PostsPageFooter.vuetest/assets/router-helper.test.tscomponents/layout/modal/ReviewPrompt.vuecomposables/useLazyToast.tstest/server/fix-canonical-queries.test.tscomponents/pages/posts/post/PostTagsToggleFallback.vuepages/terms-of-service.vuepages/privacy-policy.vuepages/premium/sign-in.vuepages/premium/index.vueplugins/035.sentry.client.tspages/cookie-policy.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuepages/settings.vueassets/js/RouterHelper.tslayouts/default.vuecomposables/useInteractionDetector.tspages/premium/backup.vuecomponents/layout/navigation/Navbar.vueassets/js/nuxt-image/imgproxy.provider.tspages/premium/tag-collections.vueplugins/040.matomo.client.tspages/premium/saved-posts/[domain].vuepages/tags/[domain]/[tag].vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/SidebarWrapper.vuepages/index.vuecomposables/useDialogManagerState.tscomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/home/FeaturedTags.vueapp/router.options.tspages/premium/dashboard.vuecomponents/layout/DialogManager.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuepages/premium/additional-boorus.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostComponent.vuepages/posts/[domain].vuenuxt.config.jstest/pages/posts.test.ts
**/*.{js,ts}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer
URL.canParse()orURL.parse()over constructortry/catchfor URL validation/parsing; useURL.parse()when parsed object is needed withURL.canParse()fallback in browser code
Files:
composables/navigation/useMenu.tscomposables/useAdvertisements.tstest/helper.tstest/server/path.test.tsserver/api/_sitemap-urls.tscomposables/useFaviconUrl.tsconfig/i18n.tstest/server-mocks/plugin.tsplugins/050.formbricks.client.tscomposables/useIdleTask.tsassets/js/sidebarLinks.tstest/router.options.test.tstest/assets/router-helper.test.tscomposables/useLazyToast.tstest/server/fix-canonical-queries.test.tsplugins/035.sentry.client.tsassets/js/RouterHelper.tscomposables/useInteractionDetector.tsassets/js/nuxt-image/imgproxy.provider.tsplugins/040.matomo.client.tscomposables/useDialogManagerState.tsapp/router.options.tsnuxt.config.jstest/pages/posts.test.ts
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Register Vue components without path prefix (use
<DomainSelector>not<Input/DomainSelector>) due topathPrefix: falsein nuxt.config.jsGenerate images at 1x density only in webp format using the custom imgproxy provider to reduce bandwidth
Keep
@formkit/auto-animateroute-scoped rather than global unless it is used broadly; use localvAutoAnimateimports on premium CSR pagesKeep breadcrumb item URLs local/locale-relative; do not convert to
project.urls.production
Files:
components/pages/posts/post/PostShareFallback.vuecomponents/pages/posts/post/PostSourceFallback.vuecomponents/pages/home/PageHistorySection.vuecomponents/pages/posts/post/PostChatWithAiFallback.vuecomponents/shared/ShareButton.vuecomponents/pages/posts/post/PostDownloadFallback.vuecomponents/pages/posts/post/PostSaveFallback.vuecomponents/pages/home/Newsletter.vuecomponents/pages/home/PageHistory.vuecomponents/pages/posts/post/PostTag.vuecomponents/layout/ClientToaster.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/layout/modal/ReviewPrompt.vuecomponents/pages/posts/post/PostTagsToggleFallback.vuecomponents/layout/navigation/Sidebar.vuecomponents/pages/posts/navigation/DomainSelectorFallback.vuecomponents/layout/navigation/Navbar.vuecomponents/shared/ContentContainer.vuecomponents/layout/navigation/SidebarWrapper.vuecomponents/pages/posts/navigation/search/TagCollections.vuecomponents/pages/home/FeaturedTags.vuecomponents/layout/DialogManager.vuecomponents/pages/posts/navigation/DomainSelector.vuecomponents/pages/posts/post/PostSource.vuecomponents/pages/posts/post/PostMedia.vuecomponents/pages/posts/post/PostComponent.vue
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Mock server-side API calls via test-only Nitro plugin at
test/server-mocks/plugin.tsinjected through nuxt.config.js$test.nitro.plugins
Files:
test/helper.tstest/server/path.test.tstest/server-mocks/plugin.tstest/router.options.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
test/**/*.test.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Write tests using
@nuxt/test-utilswith Playwright insidedescribeblocks that callawait setup({ browser: true })
Files:
test/server/path.test.tstest/router.options.test.tstest/assets/router-helper.test.tstest/server/fix-canonical-queries.test.tstest/pages/posts.test.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use flat bracket keys for query filters (e.g.,
filter[sort],filter[rating]) with Vue Router's default query handling; do not addqslibrary for nested objectsApply page-specific meta tags (title, description) using
useSeoMetain the page componentUse
@nuxt/imagev2 preload API withpreload: { fetchPriority: 'high' }for image preload priority instead of patching rendered HTML in Nitro
Files:
pages/legal.vuepages/other-sites.vuepages/premium/forgot-password.vuepages/dmca.vuepages/terms-of-service.vuepages/privacy-policy.vuepages/premium/sign-in.vuepages/premium/index.vuepages/cookie-policy.vuepages/settings.vuepages/premium/backup.vuepages/premium/tag-collections.vuepages/premium/saved-posts/[domain].vuepages/tags/[domain]/[tag].vuepages/index.vuepages/premium/dashboard.vuepages/premium/additional-boorus.vuepages/posts/[domain].vue
i18n/locales/**/*.json
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt i18n locales defined in
config/i18n.tsas single source of truth for locale configuration
Files:
i18n/locales/en.jsoni18n/locales/ru.jsoni18n/locales/fr.jsoni18n/locales/ja.jsoni18n/locales/de.jsoni18n/locales/es.jsoni18n/locales/pt.json
plugins/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep the global TanStack Vue Query plugin unless a larger measured payoff appears from route-scoping it
Files:
plugins/050.formbricks.client.tsplugins/035.sentry.client.tsplugins/040.matomo.client.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T21:26:45.770Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; verify performance changes with production build, tests, traces, and Lighthouse before deciding to keep them
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T21:26:45.770Z
Learning: Do not add app-level HTML compression; Cloudflare handles Brotli compression in production
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T21:26:45.770Z
Learning: Use Node >= 24 and pnpm >= 11.1 as specified in package.json
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-20T21:26:45.770Z
Learning: Clone repository with `--recursive` flag to include Git submodule at `assets/lib/rule-34-shared-resources`
| id: 'blog', | ||
| nameKey: 'nav.blog', | ||
| icon: NewspaperIcon, | ||
| href: `${project.urls.production.toString()}blog`, |
There was a problem hiding this comment.
Build blog URL with new URL() instead of string concatenation.
'${project.urls.production.toString()}blog' is brittle if the base format changes (trailing slash assumptions). Prefer URL composition for guaranteed correctness.
Suggested fix
- href: `${project.urls.production.toString()}blog`,
+ href: new URL('blog', project.urls.production).toString(),📝 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.
| href: `${project.urls.production.toString()}blog`, | |
| href: new URL('blog', project.urls.production).toString(), |
🤖 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 `@assets/js/sidebarLinks.ts` at line 44, The href construction using string
concatenation is brittle; update the link construction in the sidebarLinks entry
(where href is set) to build the blog URL via URL composition using the base
origin (project.urls.production) and the relative path "blog" (e.g., new
URL('blog', project.urls.production)) so it correctly handles trailing slashes
and different base formats; locate the href assignment in
assets/js/sidebarLinks.ts and replace the concatenation with a URL-built value,
then convert to string for the final href.
| <button | ||
| :aria-label="$t('common.chatWithAi')" | ||
| class="flex items-center gap-1 rounded-md px-1.5 py-1" | ||
| disabled | ||
| tabindex="-1" | ||
| type="button" | ||
| > | ||
| <SparklesIcon | ||
| aria-hidden="true" | ||
| class="text-base-content h-5 w-5" | ||
| /> | ||
|
|
||
| <span class="text-base-content text-sm font-medium">AI</span> | ||
| </button> |
There was a problem hiding this comment.
Non-interactive fallback should be hidden from the accessibility tree.
This control is permanently disabled/non-tabbable but still announced as a button via aria-label. Prefer a decorative wrapper (<span aria-hidden="true">) instead of an interactive element.
Suggested minimal change
- <button
- :aria-label="$t('common.chatWithAi')"
+ <span
+ aria-hidden="true"
class="flex items-center gap-1 rounded-md px-1.5 py-1"
- disabled
- tabindex="-1"
- type="button"
>
@@
- </button>
+ </span>📝 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.
| <button | |
| :aria-label="$t('common.chatWithAi')" | |
| class="flex items-center gap-1 rounded-md px-1.5 py-1" | |
| disabled | |
| tabindex="-1" | |
| type="button" | |
| > | |
| <SparklesIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| <span class="text-base-content text-sm font-medium">AI</span> | |
| </button> | |
| <span | |
| aria-hidden="true" | |
| class="flex items-center gap-1 rounded-md px-1.5 py-1" | |
| > | |
| <SparklesIcon | |
| aria-hidden="true" | |
| class="text-base-content h-5 w-5" | |
| /> | |
| <span class="text-base-content text-sm font-medium">AI</span> | |
| </span> |
🤖 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 `@components/pages/posts/post/PostChatWithAiFallback.vue` around lines 6 - 19,
The fallback UI currently renders a non-interactive <button> (in
PostChatWithAiFallback.vue) with an aria-label and disabled/tabindex attributes
which keeps it in the accessibility tree; change it to a purely decorative
wrapper (e.g., replace the <button> with a <span aria-hidden="true">) so
assistive tech ignores it, remove aria-label/disabled/tabindex, and preserve the
existing classes and child nodes (SparklesIcon and the inner "AI" text span) so
visual styling remains unchanged.
| const firstPostsPageAsSchema = computed(() => { | ||
| if (!data.value?.pages.length) { | ||
| return [] | ||
| } | ||
|
|
||
| return data.value?.pages[0].data.map((post) => { | ||
| switch (post.media_type) { | ||
| case 'image': | ||
| case 'animated': | ||
| return defineImage({ | ||
| url: post.high_res_file.url, | ||
|
|
||
| height: post.high_res_file.height, | ||
| width: post.high_res_file.width | ||
|
|
||
| // author: post.tags.artist.map((tag) => tag.name).join(', ') | ||
| }) | ||
|
|
||
| case 'video': | ||
| return defineVideo({ | ||
| url: post.high_res_file.url, | ||
|
|
||
| thumbnailUrl: post.preview_file.url, | ||
|
|
||
| height: post.high_res_file.height, | ||
| width: post.high_res_file.width, | ||
|
|
||
| // Unknown, so default to 0 | ||
| uploadDate: '1970-01-01', | ||
|
|
||
| isFamilyFriendly: false | ||
| }) | ||
| default: | ||
| return | ||
| const firstPagePosts = data.value?.pages[0]?.data ?? [] | ||
|
|
||
| return firstPagePosts.slice(0, 8).map((post) => { | ||
| if (post.media_type === 'video') { | ||
| return defineVideo({ | ||
| url: post.high_res_file.url, | ||
| thumbnailUrl: post.preview_file.url, | ||
| height: post.high_res_file.height, | ||
| width: post.high_res_file.width, | ||
| isFamilyFriendly: false | ||
| }) | ||
| } | ||
|
|
||
| return defineImage({ | ||
| url: post.high_res_file.url, | ||
| height: post.high_res_file.height, | ||
| width: post.high_res_file.width, | ||
| caption: [...post.tags.character, ...post.tags.copyright].join(', '), | ||
| author: post.tags.artist.length ? post.tags.artist.join(', ') : undefined, | ||
| isFamilyFriendly: false | ||
| }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Schema generation assumes high_res_file is always present.
The firstPostsPageAsSchema computed accesses post.high_res_file.url, post.high_res_file.height, etc. without null checks. If a post lacks high_res_file data, this will throw at runtime during schema generation.
🛡️ Proposed defensive fix
const firstPostsPageAsSchema = computed(() => {
const firstPagePosts = data.value?.pages[0]?.data ?? []
- return firstPagePosts.slice(0, 8).map((post) => {
+ return firstPagePosts
+ .filter((post) => post.high_res_file?.url)
+ .slice(0, 8)
+ .map((post) => {
if (post.media_type === 'video') {🤖 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 `@pages/posts/`[domain].vue around lines 872 - 895, The computed
firstPostsPageAsSchema assumes post.high_res_file exists; update the
firstPostsPageAsSchema computed to defensively handle missing high_res_file by
first filtering or guarding posts: for each post check post.high_res_file (and
for videos also post.preview_file) before calling defineVideo/defineImage, using
optional chaining (post.high_res_file?.url) and sensible fallbacks (e.g.,
post.preview_file?.url) or skipping posts that lack required assets; ensure you
reference the symbols firstPostsPageAsSchema, defineVideo, defineImage, and the
post.high_res_file/post.preview_file properties when implementing the guard so
schema generation never accesses undefined properties.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.backupPage.seoTitle'), item: localePath('/premium/backup') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Add a description meta field in useSeoMeta.
Line 67 currently sets only title. This page should also set description for page-level SEO consistency.
Suggested patch
useSeoMeta({
- title: computed(() => t('pages.premium.backupPage.seoTitle'))
+ title: computed(() => t('pages.premium.backupPage.seoTitle')),
+ description: computed(() => t('pages.premium.backupPage.pageDescription'))
})As per coding guidelines: "pages/**/*.vue: Apply page-specific meta tags (title, description) using useSeoMeta in the page component"
🤖 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 `@pages/premium/backup.vue` around lines 71 - 79, The page currently only sets
a title via useSeoMeta; update the page component to also pass a description to
useSeoMeta (e.g., description: t('pages.premium.backupPage.seoDescription')) so
page-level SEO includes both title and description; modify the call to
useSeoMeta in pages/premium/backup.vue (alongside the existing title) and ensure
the translation key exists and is used consistently with the existing t(...)
calls.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.savedPostsPage.title'), item: route.path } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Include description in useSeoMeta for this page.
Line 614 only sets title; please add description to complete page SEO metadata.
Suggested patch
useSeoMeta({
- title: shortTitle
+ title: shortTitle,
+ description: computed(() => t('posts.seo.savedPosts'))
})As per coding guidelines: "pages/**/*.vue: Apply page-specific meta tags (title, description) using useSeoMeta in the page component"
🤖 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 `@pages/premium/saved-posts/`[domain].vue around lines 618 - 626, The page sets
only the title but omits the description in SEO metadata; update the component
to call useSeoMeta with both title and description (e.g., useSeoMeta({ title:
t('pages.premium.savedPostsPage.title'), description:
t('pages.premium.savedPostsPage.description') })) so the page has complete meta
tags; locate the existing useSeoMeta usage near the
useSchemaOrg/defineBreadcrumb block and add the description entry using the
appropriate i18n key (t) and route/locale helpers as needed.
| useSchemaOrg([ | ||
| defineBreadcrumb({ | ||
| itemListElement: [ | ||
| { name: t('nav.home'), item: localePath('/') }, | ||
| { name: t('pages.premium.landingPage.seoTitle'), item: localePath('/premium') }, | ||
| { name: t('pages.premium.tagCollectionsPage.seoTitle'), item: localePath('/premium/tag-collections') } | ||
| ] | ||
| }) | ||
| ]) |
There was a problem hiding this comment.
Add a description entry to this page’s useSeoMeta.
Line 143 currently only sets title; include description as well to satisfy page SEO meta requirements.
Suggested patch
useSeoMeta({
- title: computed(() => t('pages.premium.tagCollectionsPage.seoTitle'))
+ title: computed(() => t('pages.premium.tagCollectionsPage.seoTitle')),
+ description: computed(() => t('pages.premium.tagCollectionsPage.pageDescription'))
})As per coding guidelines: "pages/**/*.vue: Apply page-specific meta tags (title, description) using useSeoMeta in the page component"
🤖 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 `@pages/premium/tag-collections.vue` around lines 147 - 155, The page currently
calls useSeoMeta with only a title; update the useSeoMeta call to include a
description property as well (e.g. description:
t('pages.premium.tagCollectionsPage.seoDescription')) so the component sets both
title and description for SEO; locate the useSeoMeta invocation near
useSchemaOrg and add the description key using the appropriate i18n translation
string or a literal description if no translation key exists.
| await page.evaluate(async () => { | ||
| window.scrollTo(0, 0) | ||
| await new Promise((resolve) => requestAnimationFrame(resolve)) | ||
|
|
||
| for (let i = 0; i < 20; i++) { | ||
| window.scrollTo(0, document.documentElement.scrollHeight) | ||
| window.dispatchEvent(new Event('scroll')) | ||
| await new Promise((resolve) => requestAnimationFrame(resolve)) | ||
| } | ||
| }) | ||
| await page.mouse.wheel(0, 100000) | ||
| await page.waitForURL((u) => u.searchParams.get('tags') === 'hair_bun' && u.searchParams.get('page') !== '0', { | ||
| timeout: 10000 | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | 💤 Low value
Consider adding a comment explaining the large wheel delta.
The mouse.wheel(0, 100000) uses a very large delta. While this likely ensures pagination triggers in the test environment, a brief comment explaining why would help future maintainers.
🤖 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/pages/posts.test.ts` around lines 320 - 333, Add a brief inline comment
above the page.mouse.wheel(0, 100000) call explaining why such a large vertical
delta is used (e.g., to force a full-page scroll/pagination trigger in headless
test environments where smaller deltas may not reach the threshold), and note
any alternatives or caveats (like viewport-size dependence) so future
maintainers understand the intent; update the comment near the
page.evaluate(...) block if that context is clearer and reference the
page.mouse.wheel invocation and the surrounding scroll loop in posts.test.ts for
placement.
| import { describe, expect, it } from 'vitest' | ||
| import routerOptions from '../app/router.options' | ||
|
|
||
| const route = ({ path = '/posts/safebooru.org', hash = '', query = {} } = {}) => | ||
| ({ | ||
| path, | ||
| hash, | ||
| query | ||
| }) as any | ||
|
|
||
| describe('router scroll behavior', () => { | ||
| const scrollBehavior = routerOptions.scrollBehavior! | ||
|
|
||
| it('keeps scroll position when only the page query changes on the same route', () => { | ||
| expect(scrollBehavior(route({ query: { page: '2' } }), route({ query: { page: '1' } }), null)).toBeUndefined() | ||
| }) | ||
|
|
||
| it('scrolls to top when page changes together with another query value', () => { | ||
| expect( | ||
| scrollBehavior(route({ query: { page: '1', tags: '1girl' } }), route({ query: { page: '2' } }), null) | ||
| ).toEqual({ left: 0, top: 0 }) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Migrate this test to @nuxt/test-utils browser setup to match test standard.
This *.test.ts file currently uses plain Vitest only, but repo policy requires Nuxt test-utils with await setup({ browser: true }) inside describe blocks.
As per coding guidelines: "test/**/*.test.{ts,js}: Write tests using @nuxt/test-utils with Playwright inside describe blocks that call await setup({ browser: true })."
🤖 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/router.options.test.ts` around lines 1 - 23, The tests in
router.options.test.ts use plain Vitest; update them to use `@nuxt/test-utils`
browser setup by importing setup from '`@nuxt/test-utils`' and making the describe
callback async so you can call await setup({ browser: true }) before running
assertions; keep the existing route helper and reuse
routerOptions.scrollBehavior in the tests but wrap the current test body inside
the describe after the awaited setup call so the Playwright browser environment
is initialized per the repo policy.
left a comment
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 `@assets/js/RouterHelper.ts`:
- Around line 63-68: The code calls tag.split(...) before checking tag for
null/undefined, which can throw; fix by guarding tag first — move the
truthy/null check (the existing if that tests !tag, tag.startsWith('-'),
tag.includes('|'), /\s/.test(tag), and facetedTagPrefixes.has(...)) to run
before computing tagPrefix, and only compute tagPrefix = tag.split(':', 1)[0]
after confirming tag is non-null/defined and passes the other quick checks (or
alternately use a safe access when deriving tagPrefix). Reference:
getSingleQueryValue, tag, tagPrefix, facetedTagPrefixes.
- Line 75: In getTagLandingPathFromPostsQueryPath replace the direct constructor
call that creates parsed (currently "const parsed = new URL(path,
'https://example.com')") with the repo's safe parser: call URL.parse(path,
'https://example.com') and assign to parsed; then check if parsed is null and
return undefined when it is, otherwise continue using parsed as before to avoid
constructor exceptions on malformed input.
🪄 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: 2109d35c-5648-4cac-a16a-1f3eceecae66
📒 Files selected for processing (13)
AGENTS.mdassets/js/RouterHelper.tscomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/PostsPageFooter.vuecomponents/pages/posts/post/PostComponent.vuenuxt.config.jspages/posts/[domain]/[tag].vuepages/posts/[domain]/index.vueserver/api/_sitemap-urls.tsserver/plugins/fix-canonical-queries.tstest/assets/router-helper.test.tstest/pages/posts.test.tstest/server/fix-canonical-queries.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (12)
**/*.{ts,tsx,vue,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt 4 with Vue 3 and TypeScript for all application code
Files:
components/pages/posts/PostsPageFooter.vueserver/plugins/fix-canonical-queries.tsassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tspages/posts/[domain]/[tag].vuecomponents/pages/home/FeaturedTags.vueserver/api/_sitemap-urls.tscomponents/pages/posts/post/PostComponent.vuetest/assets/router-helper.test.tspages/posts/[domain]/index.vuenuxt.config.jstest/pages/posts.test.ts
**/*.{ts,tsx,vue,js,css}
📄 CodeRabbit inference engine (AGENTS.md)
Format code with Prettier using 120-char print width, no semicolons, single quotes, trailing commas removed, and single attribute per line in Vue templates
Files:
components/pages/posts/PostsPageFooter.vueserver/plugins/fix-canonical-queries.tsassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tspages/posts/[domain]/[tag].vuecomponents/pages/home/FeaturedTags.vueserver/api/_sitemap-urls.tscomponents/pages/posts/post/PostComponent.vuetest/assets/router-helper.test.tspages/posts/[domain]/index.vuenuxt.config.jstest/pages/posts.test.ts
{pages,components}/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Import Vue components without path prefix using flat auto-import names (e.g., not <Input/DomainSelector>)
Files:
components/pages/posts/PostsPageFooter.vuepages/posts/[domain]/[tag].vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuepages/posts/[domain]/index.vue
**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use project.urls.production for canonical URLs to ensure they point to production domain for SEO purposes
Use imgproxy provider for NuxtImg with 1x density webp format only, not 2x density images
Files:
components/pages/posts/PostsPageFooter.vuepages/posts/[domain]/[tag].vuecomponents/pages/home/FeaturedTags.vuecomponents/pages/posts/post/PostComponent.vuepages/posts/[domain]/index.vue
**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
Prefer URL.canParse() or URL.parse() over constructor try/catch for URL validation and parsing
Files:
server/plugins/fix-canonical-queries.tsassets/js/RouterHelper.tstest/server/fix-canonical-queries.test.tsserver/api/_sitemap-urls.tstest/assets/router-helper.test.tsnuxt.config.jstest/pages/posts.test.ts
server/plugins/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place Nitro plugins in server/plugins/ directory for server-side initialization
Files:
server/plugins/fix-canonical-queries.ts
server/plugins/fix-canonical-queries.ts
📄 CodeRabbit inference engine (AGENTS.md)
Use server/plugins/fix-canonical-queries.ts to patch canonical in SSR rendered HTML for i18n canonical workaround
Files:
server/plugins/fix-canonical-queries.ts
assets/js/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Store shared JavaScript utilities, DTOs, and custom providers in assets/js/ directory
Files:
assets/js/RouterHelper.ts
test/**/*.{test,spec}.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest with
@nuxt/test-utilsand Playwright browser mode for testing
Files:
test/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
test/**/*.{test,spec}.ts
📄 CodeRabbit inference engine (AGENTS.md)
Use
@nuxt/test-utilswith Playwright inside describe blocks that call await setup({ browser: true }) for browser testingMock server-side API calls via test-only Nitro plugin at test/server-mocks/plugin.ts injected through nuxt.config.js
Use debugBrowserOptions from test/helper.ts for headful Playwright playback with slowMo during test debugging
Files:
test/server/fix-canonical-queries.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Use flat bracket keys for query filters (filter[sort], filter[rating], etc.) and avoid re-adding qs library unless URL contract changes
Keep
@formkit/auto-animateroute-scoped unless it is used broadly; use local vAutoAnimate imports on premium CSR pages
Files:
pages/posts/[domain]/[tag].vuepages/posts/[domain]/index.vue
server/api/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Place Nitro server API routes in server/api/ directory following Nitro route conventions
Configure external API calls to NUXT_PUBLIC_API_URL (default http://localhost:8081)
Files:
server/api/_sitemap-urls.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Prefer high-impact, measurable performance optimizations over small rewrites; verify with production builds and Lighthouse before committing
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Do not enable app-level HTML compression as production is behind Cloudflare with Brotli compression
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Keep the global TanStack Vue Query plugin unless a larger measured payoff appears for removing it
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
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-22T08:06:25.740Z
Learning: Upload source maps to Sentry only in production Docker builds with SENTRY_ORG, SENTRY_PROJECT, and SENTRY_AUTH_TOKEN
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Use pnpm install --frozen-lockfile in Docker production build stage; set SENTRY_UPLOAD_SOURCE_MAPS=false to skip source map uploads
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Use NITRO_PRESET build arg to select deployment target in Docker production build
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Validate image delivery in environments where imgproxy can resolve source URLs; non-premium SPA navigations keep direct image paths
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Ensure Node >= 24 is used as required by package.json engines
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Ensure pnpm >= 11.1 is used as pinned by packageManager in package.json
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:06:25.740Z
Learning: Clone repository with --recursive to include Git submodule at assets/lib/rule-34-shared-resources
🔇 Additional comments (12)
AGENTS.md (1)
67-68: LGTM!components/pages/home/FeaturedTags.vue (1)
3-3: LGTM!Also applies to: 52-55, 105-105
components/pages/posts/PostsPageFooter.vue (1)
5-5: LGTM!Also applies to: 126-126
components/pages/posts/post/PostComponent.vue (1)
3-3: LGTM!Also applies to: 290-290
nuxt.config.js (1)
17-44: LGTM!Also applies to: 185-194, 270-271
pages/posts/[domain]/[tag].vue (1)
196-196: LGTM!pages/posts/[domain]/index.vue (1)
13-14: LGTM!Also applies to: 857-875
server/api/_sitemap-urls.ts (1)
1-1: LGTM!Also applies to: 22-32
server/plugins/fix-canonical-queries.ts (1)
23-23: LGTM!Also applies to: 36-63, 65-83, 92-99, 112-112
test/assets/router-helper.test.ts (1)
2-8: LGTM!Also applies to: 75-99
test/pages/posts.test.ts (1)
408-425: LGTM!Also applies to: 441-454, 495-533
test/server/fix-canonical-queries.test.ts (1)
24-28: LGTM!Also applies to: 36-40, 54-58, 77-77, 82-82
left a comment
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 `@test/pages/posts.test.ts`:
- Around line 249-266: The test currently uses an arbitrary delay
(page.waitForTimeout) after reaching page=1 which can be flaky; instead wait
deterministically for the next page content to render before reading
window.scrollY. Update the test around the
page.route/holdNextPageResponse/mockPostsPage1 flow to await a concrete signal
such as waiting for a new post element or virtualizer item to appear (e.g., use
page.waitForSelector for a post from mockPostsPage1 or wait for the network
response that fulfills the route) after releasing holdNextPageResponse; ensure
you reference the existing page.route handler and the mockPostsPage1 response
when choosing the selector or response predicate so the test only proceeds once
the new content is rendered.
🪄 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: 27cd43e2-67ab-4630-b7d2-c041e14c4eb1
📒 Files selected for processing (6)
assets/js/RouterHelper.tscomponents/pages/home/FeaturedTags.vuepages/posts/[domain]/index.vuetest/assets/router-helper.test.tstest/pages/index.test.tstest/pages/posts.test.ts
💤 Files with no reviewable changes (1)
- assets/js/RouterHelper.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{vue,ts,tsx,js,mjs}
📄 CodeRabbit inference engine (AGENTS.md)
Use Nuxt 4 with Vue 3 and TypeScript for the application framework
Files:
test/pages/index.test.tscomponents/pages/home/FeaturedTags.vuepages/posts/[domain]/index.vuetest/assets/router-helper.test.tstest/pages/posts.test.ts
test/**/*.{ts,tsx,js}
📄 CodeRabbit inference engine (AGENTS.md)
Use Vitest with
@nuxt/test-utilsand Playwright browser mode for testingTests must use
@nuxt/test-utilswith Playwright inside describe blocks that call await setup({ browser: true })For debug testing, import debugBrowserOptions from test/helper.ts for headful playback with slowMo
Files:
test/pages/index.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
**/*.{ts,tsx,js,mjs,vue}
📄 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:
test/pages/index.test.tscomponents/pages/home/FeaturedTags.vuepages/posts/[domain]/index.vuetest/assets/router-helper.test.tstest/pages/posts.test.ts
**/*.{ts,tsx,js,mjs}
📄 CodeRabbit inference engine (AGENTS.md)
For URL validation/parsing, prefer URL.canParse() or URL.parse() over constructor try/catch
Files:
test/pages/index.test.tstest/assets/router-helper.test.tstest/pages/posts.test.ts
**/*.{css,vue}
📄 CodeRabbit inference engine (AGENTS.md)
Use TailwindCSS v4 via
@tailwindcss/viteVite plugin (NOT PostCSS) for styling
Files:
components/pages/home/FeaturedTags.vuepages/posts/[domain]/index.vue
components/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Components are auto-imported without path prefix; import them as not <Input/DomainSelector>
Images should be generated at 1x density only in webp format to reduce bandwidth
Files:
components/pages/home/FeaturedTags.vue
pages/**/*.vue
📄 CodeRabbit inference engine (AGENTS.md)
Page-specific SEO tags (title, description) should use useSeoMeta in the page component
Keep
@formkit/auto-animateroute-scoped unless used broadly; use local vAutoAnimate imports on premium CSR pages
Files:
pages/posts/[domain]/index.vue
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Canonical URLs must point to production (https://r34.app/…) even when served from clone domains; use project.urls.production
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Schema.org breadcrumb item URLs should stay local/locale-relative; do not convert breadcrumb items to project.urls.production
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Do not re-add qs for nested route.query.filter objects; use flat bracket keys (filter[sort], filter[rating]) with Vue Router's default query handling
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Prefer high-impact, measurable optimizations over small rewrites; keep battle-tested dependencies unless replacing one has clear measured payoff
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Verify performance changes with production build, relevant tests, request traces, and Lighthouse before deciding changes are worth keeping
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Keep global TanStack Vue Query plugin unless larger measured payoff appears
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
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-22T08:33:35.572Z
Learning: Source map uploads only happen in production Docker builds with SENTRY_ORG, SENTRY_PROJECT, and SENTRY_AUTH_TOKEN; set SENTRY_UPLOAD_SOURCE_MAPS=false to skip
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Docker production build must use multi-stage with pnpm install --frozen-lockfile in build stage
Learnt from: CR
Repo: Rule-34/App
Timestamp: 2026-05-22T08:33:35.572Z
Learning: Docker production stage should copy only .output/ (no node_modules needed since Nitro bundles everything)
🔇 Additional comments (7)
test/pages/posts.test.ts (2)
277-277: 💤 Low valueArbitrary
waitForTimeoutmay cause flakiness.
waitForTimeout(100)is a magic number wait. Consider using a more deterministic approach like waiting for a specific condition or network state, or add a comment explaining why this delay is necessary.
239-284: New scroll-position retention test looks well-structured.The test properly sets up route interception, triggers scrolling deterministically, and verifies both the scroll position and end-state indicator. The pattern is consistent with other pagination tests in this file.
test/pages/index.test.ts (1)
34-43: LGTM!components/pages/home/FeaturedTags.vue (1)
51-54: LGTM!pages/posts/[domain]/index.vue (1)
485-489: LGTM!test/assets/router-helper.test.ts (2)
6-6: LGTM!
74-90: LGTM!
| await page.route( | ||
| '**/booru/gelbooru/posts*pageID=1*', | ||
| async (route) => { | ||
| await holdNextPageResponse | ||
|
|
||
| await route.fulfill({ | ||
| status: 200, | ||
| json: { | ||
| ...mockPostsPage1, | ||
| links: { | ||
| ...mockPostsPage1.links, | ||
| next: null | ||
| } | ||
| } | ||
| }) | ||
| }, | ||
| { times: 1 } | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check what booru type safebooru.org uses in the codebase
rg -n "safebooru\.org" --type=ts -C 3 | head -100
# Also check the booru list configuration
fd -i "booru" --extension ts --extension js | xargs rg -l "safebooru" | head -5 | xargs cat 2>/dev/null | head -200Repository: Rule-34/App
Length of output: 7141
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the referenced portion of the test
sed -n '220,320p' test/pages/posts.test.ts
# Find the page navigation + the API request the app makes for safebooru.org (if any direct mapping exists)
rg -n "safebooru\.org" test/pages/posts.test.ts -n
# Look for how "baseEndpoint" is used/mapped to a booru API type (gelbooru, etc.)
rg -n "baseEndpoint=safebooru\.org" -S test src server app || true
rg -n "safebooru\.org" -S app server src test | head -200Repository: Rule-34/App
Length of output: 37451
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Confirm how safebooru.org is wired to booru API endpoints
rg -n "baseEndpoint=safebooru\.org" -S --hidden .
rg -n "safebooru\.org.*gelbooru|gelbooru.*safebooru\.org" -S --hidden .
rg -n "/booru/gelbooru/posts" test/pages/posts.mock-data.ts test/server/booru-mocks.test.ts
# Check how the test handles deferred pagination (and whether waitForTimeout(100) is used elsewhere)
rg -n "waitForTimeout\\(100\\)" test/pages/posts.test.ts test/pages -S
rg -n "holdNextPageResponse|releaseNextPageResponse" test/pages/posts.test.ts -SRepository: Rule-34/App
Length of output: 8168
Route intercept pattern matches the safebooru.org pagination request; only minor flakiness risk remains
safebooru.orgrequests are mocked via thegelbooruAPI (/booru/gelbooru/posts?baseEndpoint=safebooru.org&pageID=1...), so**/booru/gelbooru/posts*pageID=1*should trigger correctly.await page.waitForTimeout(100)after reachingpage=1is an arbitrary delay; wait for a deterministic signal that the next page content/virtualizer has rendered (e.g., a new post visible) before readingwindow.scrollY.
🤖 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/pages/posts.test.ts` around lines 249 - 266, The test currently uses an
arbitrary delay (page.waitForTimeout) after reaching page=1 which can be flaky;
instead wait deterministically for the next page content to render before
reading window.scrollY. Update the test around the
page.route/holdNextPageResponse/mockPostsPage1 flow to await a concrete signal
such as waiting for a new post element or virtualizer item to appear (e.g., use
page.waitForSelector for a post from mockPostsPage1 or wait for the network
response that fulfills the route) after releasing holdNextPageResponse; ensure
you reference the existing page.route handler and the mockPostsPage1 response
when choosing the selector or response predicate so the test only proceeds once
the new content is rendered.
Add an early guard in getSinglePositiveTagQueryValue to return undefined for falsy or malformed tags (empty, undefined, null, starting with '-', containing '|', or whitespace) before splitting to extract the prefix. Keep the existing faceted-prefix check afterwards. Extend unit tests to cover undefined, null, and array-with-null inputs to prevent runtime errors and ensure invalid inputs are handled consistently.
Summary
Testing
Notes
Summary by CodeRabbit
New Features
Improvements
Changes
Tests