Conversation
WalkthroughThis PR adds schema.org structured metadata across multiple components (ReviewCard, ProfileCard, page template) to enhance semantic HTML and SEO. It introduces an app URL configuration property and integrates useSchemaOrg() composables to emit Review, Person, LocalBusiness, and AggregateRating metadata while formatting review timestamps as ISO datetime strings. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12–18 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
apps/web-app/app/components/user/ProfileCard.vue (2)
2-7: Align schema.org Person URL with https and with ReviewCardThe root element uses
itemtype="http://schema.org/Person"while the review card useshttps://schema.org/Review. Consider switching this tohttps://schema.org/Personfor consistency and to follow current schema.org/SEO recommendations.
17-22: Image/name microdata looks good; minor cleanup possibleThe
itemprop="image"+itemprop="name"usage is semantically correct. The expressionuser.avatarUrl ?? undefinedis a bit redundant, as passingnull/undefinedto a bound attribute in Vue already drops the attribute; you could simplify to:content="user.avatarUrl"if you like.apps/web-app/app/components/page/ReviewCard.vue (2)
61-69: Unify schema.org URL and validate rating scaleThe
reviewRatingblock is structured well, but:
- You’re using
itemtype="http://schema.org/Rating"here while the root useshttps://schema.org/Review. Consider switching this tohttps://schema.org/Ratingfor consistency.- You hard‑code
bestRating="5"andworstRating="1". That’s fine if your domain guarantees a 1–5 scale; if0is ever valid, this would be misleading, so worth double‑checking.
87-92: Consider including pros/cons in thereviewBodyfor richer structured dataCurrently only the “Комментарий” section is tagged with
itemprop="reviewBody", while “Достоинства” and “Недостатки” are left unannotated. For search engines,reviewBodyis expected to hold the main textual content of the review; you may get better snippets if all textual sections (pros, cons, comment) are encompassed in a singlereviewBodycontainer or otherwise combined.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web-app/app/components/page/ReviewCard.vue(3 hunks)apps/web-app/app/components/user/ProfileCard.vue(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: cubic · AI code reviewer
- GitHub Check: build
🔇 Additional comments (1)
apps/web-app/app/components/page/ReviewCard.vue (1)
94-99: The datetime attribute already uses a safe format.
review.createdAtis configured in the database schema with Drizzle'smode: 'string', which means it's returned from the database as an ISO 8601 string—not aDateobject. Therefore,:datetime="review.createdAt"is already machine-readable and consistent with thedatePublishedmeta tag. No changes are needed.Likely an incorrect or invalid review comment.
| <div | ||
| class="border border-default rounded-lg p-4 sm:p-5 flex flex-col gap-5 group/card" | ||
| itemscope | ||
| itemprop="review" | ||
| itemtype="https://schema.org/Review" | ||
| > |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the full ReviewCard.vue file
cat -n apps/web-app/app/components/page/ReviewCard.vueRepository: k39space/k39
Length of output: 5425
🏁 Script executed:
# Search for files that import or use ReviewCard
rg "ReviewCard" --type vue --type js --type ts -lRepository: k39space/k39
Length of output: 83
🏁 Script executed:
# Search for ReviewCard usage across all file types
rg "ReviewCard" --type-list | grep -E '\.(vue|ts|js)' || echo "Checking available types..."
rg "ReviewCard" --max-count 50Repository: k39space/k39
Length of output: 949
🏁 Script executed:
# Look for files importing ReviewCard
fd "ReviewCard" --type fRepository: k39space/k39
Length of output: 264
🏁 Script executed:
cat -n apps/web-app/app/pages/[pageSlug]/reviews.vueRepository: k39space/k39
Length of output: 2779
🏁 Script executed:
# Check if there are any other places where ReviewCard is used (besides reviews.vue)
rg "PageReviewCard" --max-count 20Repository: k39space/k39
Length of output: 514
🏁 Script executed:
# Check if the main page component has schema markup that might wrap reviews
find apps/web-app/app/pages -name "*\[pageSlug\]*" -type f | head -10Repository: k39space/k39
Length of output: 142
🏁 Script executed:
# Check the main page component to see if there's any schema markup
cat -n apps/web-app/app/pages/[pageSlug].vue | head -100Repository: k39space/k39
Length of output: 2754
Remove itemprop="review" from the root Review wrapper
The root div is both an item (itemscope itemtype="https://schema.org/Review") and a property value via itemprop="review". According to schema.org microdata spec, itemprop="review" is only valid when this element is nested inside a parent itemscope (e.g., Product, Organization). Since ReviewCard is rendered standalone without a parent itemscope wrapper in reviews.vue, remove itemprop="review" and keep only itemscope and itemtype to expose a clean top-level Review item.
🤖 Prompt for AI Agents
In apps/web-app/app/components/page/ReviewCard.vue around lines 2 to 7, the root
div currently has itemprop="review" while also declaring itemscope and itemtype,
which is incorrect for a top-level Review item; remove the itemprop="review"
attribute from this root wrapper and leave only itemscope and itemtype so the
component exposes a top-level Review item correctly (do not add any parent
itemscope here).
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
apps/web-app/app/components/page/ReviewCard.vue (1)
2-7: Removeitemprop="review"from the root wrapper.This attribute is only valid when nested inside a parent
itemscope(e.g., Product, Organization). Since ReviewCard is rendered standalone in reviews.vue, keep onlyitemscopeanditemtype.<div class="border border-default rounded-lg p-4 sm:p-5 flex flex-col gap-5 group/card" itemscope - itemprop="review" itemtype="https://schema.org/Review" >
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web-app/app/components/page/ReviewCard.vue(4 hunks)apps/web-app/app/components/page/ReviewCardPhoto.vue(1 hunks)apps/web-app/app/components/user/ProfileCard.vue(2 hunks)apps/web-app/app/pages/[pageSlug]/reviews.vue(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- apps/web-app/app/components/page/ReviewCardPhoto.vue
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web-app/app/components/user/ProfileCard.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
apps/web-app/app/components/page/ReviewCard.vue (4)
61-69: Rating microdata looks correct.The
reviewRatingwithbestRating,worstRating, andratingValuefollows schema.org conventions properly. Using.toFixed(1)ensures consistent decimal formatting.
89-99: Headline and reviewBody annotations are well-structured.Truncating headline to 100 characters is a sensible approach for structured data. The conditional rendering with
v-ifensures no empty metadata is emitted.
102-107: Date handling is correct.Using
yyyy-MM-ddformat fordatePublishedmeta aligns with schema.org's expected ISO 8601 date format, while the visible<time>element displays a localized format for users.
143-159: Interaction statistics are properly annotated.The
InteractionCounteritems correctly specifyLikeActionandDislikeActionwith their respective counts. Using.toString()ensures content attributes receive string values.
| <span | ||
| itemscope | ||
| itemprop="itemReviewed" | ||
| itemtype="https://schema.org/LocalBusiness" | ||
| > | ||
| <meta itemprop="url" :content="`https://k39.online/${review.page.slug}`"> | ||
|
|
||
| <PageReviewCardPhoto | ||
| v-for="photo in review.photos" | ||
| :key="photo.id" | ||
| :photo="photo.photo" | ||
| :alt="`Пользовательское фото из отзыва к «${review.page.title}»`" | ||
| /> | ||
| </span> |
There was a problem hiding this comment.
itemReviewed is conditionally rendered and breaks grid layout.
Two issues:
-
Semantic issue:
itemReviewedonly appears whenreview.photos.length > 0. Every Review should declare what's being reviewed, regardless of photos. -
Layout issue: The
<span>wrapper makes all photos a single grid item instead of individual grid cells.
Consider moving itemReviewed outside the photos block (e.g., as a standalone meta near the top), and keep photo iteration as direct grid children:
+ <span
+ itemscope
+ itemprop="itemReviewed"
+ itemtype="https://schema.org/LocalBusiness"
+ >
+ <meta itemprop="url" :content="`https://k39.online/${review.page.slug}`">
+ <meta itemprop="name" :content="review.page.title">
+ </span>
+
<div v-if="review.photos.length" class="flex flex-col gap-2">
<h3 class="text-lg/5 font-bold">
Фото
</h3>
<div class="grid grid-cols-3 lg:grid-cols-5 gap-2">
- <span
- itemscope
- itemprop="itemReviewed"
- itemtype="https://schema.org/LocalBusiness"
- >
- <meta itemprop="url" :content="`https://k39.online/${review.page.slug}`">
-
- <PageReviewCardPhoto
- v-for="photo in review.photos"
- :key="photo.id"
- :photo="photo.photo"
- :alt="`Пользовательское фото из отзыва к «${review.page.title}»`"
- />
- </span>
+ <PageReviewCardPhoto
+ v-for="photo in review.photos"
+ :key="photo.id"
+ :photo="photo.photo"
+ :alt="`Пользовательское фото из отзыва к «${review.page.title}»`"
+ />
</div>
</div>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web-app/app/components/page/ReviewCard.vue around lines 115 to 128, the
current <span> carrying itemReviewed is only present when there are photos and
also wraps all photos causing them to render as a single grid item; move the
schema.org markup out of the photos block so itemReviewed (and its URL/title
meta tags) are always present for every review (place standalone meta elements
near the top of the review card) and remove the surrounding <span> so
PageReviewCardPhoto v-for renders as direct grid children (each photo stays an
individual grid cell) while preserving the same itemprop values on the
standalone meta(s).
| <span | ||
| itemscope | ||
| itemprop="aggregateRating" | ||
| itemtype="https://schema.org/AggregateRating" | ||
| > | ||
| <meta itemprop="ratingValue" :content="page?.rating.toString()"> | ||
| <meta itemprop="reviewCount" :content="page?.reviewsCount.toString()"> | ||
| <meta itemprop="bestRating" content="5"> | ||
| <meta itemprop="worstRating" content="1"> | ||
| </span> |
There was a problem hiding this comment.
aggregateRating requires a parent itemscope to be valid.
The itemprop="aggregateRating" attribute is only meaningful when nested inside an item (e.g., LocalBusiness, Product). Currently, this span is orphaned without a parent itemscope, so search engines won't associate the rating with any entity.
Additionally, page?.rating.toString() could throw if page is null—the optional chain stops at rating, but .toString() would be called on undefined.
Consider wrapping the entire page content in an itemscope for the reviewed entity:
<UContainer class="mt-4 max-w-5xl">
- <div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-10">
+ <div
+ class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-10"
+ itemscope
+ itemtype="https://schema.org/LocalBusiness"
+ >
+ <meta itemprop="name" :content="page?.title">
+ <meta itemprop="url" :content="`https://k39.online/${page?.slug}`">
<div class="col-span-2">
<div v-if="pageReviews?.length" class="flex flex-col gap-6">
<span
itemscope
itemprop="aggregateRating"
itemtype="https://schema.org/AggregateRating"
>
- <meta itemprop="ratingValue" :content="page?.rating.toString()">
- <meta itemprop="reviewCount" :content="page?.reviewsCount.toString()">
+ <meta itemprop="ratingValue" :content="page?.rating?.toString()">
+ <meta itemprop="reviewCount" :content="page?.reviewsCount?.toString()">
<meta itemprop="bestRating" content="5">
<meta itemprop="worstRating" content="1">
</span>Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web-app/app/pages/[pageSlug]/reviews.vue around lines 6 to 15, the span
with itemprop="aggregateRating" is an orphaned itemscope (no parent
itemscope/itemtype) and the rating meta uses page?.rating.toString() which can
throw if page or rating is undefined; wrap the page content (or the nearest
appropriate container for the reviewed entity) in a parent element that has
itemscope and a suitable itemtype (e.g., LocalBusiness/Product or the correct
schema.org type), move the aggregateRating span inside that parent, and change
the rating meta to guard against undefined (e.g., compute a safe string/value
before calling toString or use a conditional that only renders the meta when
page?.rating is a defined number) so search engines can associate the rating
with the entity and we avoid runtime errors.
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web-app/app/app.config.ts(1 hunks)apps/web-app/app/components/page/ReviewCard.vue(2 hunks)apps/web-app/app/components/user/ProfileCard.vue(1 hunks)apps/web-app/app/pages/[pageSlug].vue(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web-app/app/components/page/ReviewCard.vue
- apps/web-app/app/components/user/ProfileCard.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
apps/web-app/app/pages/[pageSlug].vue (1)
60-60: LGTM!The app config retrieval is correctly implemented using the Nuxt composable pattern.
| @@ -1,5 +1,6 @@ | |||
| export default defineAppConfig({ | |||
| app: { | |||
| url: 'https://k39.online', | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for the app.config.ts file
find . -type f -name "app.config.ts" | head -20Repository: k39space/k39
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Description: Verify app.config.ts concerns and check for existing environment configuration
# Step 1: Find and display app.config.ts
echo "=== Finding app.config.ts ==="
fd -t f "app\.config\.ts"
# Step 2: Display content if found
echo -e "\n=== Content of app.config.ts ==="
fd -t f "app\.config\.ts" -x cat -n {}
# Step 3: Check for runtime config usage in the codebase
echo -e "\n=== Checking for useRuntimeConfig usage ==="
rg "useRuntimeConfig" -A2 -B2 --type=ts --max-count=10
# Step 4: Check for environment variables in nuxt.config
echo -e "\n=== Checking for runtimeConfig in nuxt.config ==="
fd -t f "nuxt\.config\.(ts|js)" -x grep -n "runtimeConfig" {} -A5 -B2
# Step 5: Check for .env files
echo -e "\n=== Looking for .env files ==="
fd -t f "^\\.env"
# Step 6: Check if URL is used elsewhere
echo -e "\n=== Checking for usage of k39.online ==="
rg "k39\.online" --type=tsRepository: k39space/k39
Length of output: 4440
🏁 Script executed:
#!/bin/bash
# Check nuxt.config.ts for the URL configuration
echo "=== Full nuxt.config.ts excerpt around line 52 ==="
sed -n '40,70p' apps/web-app/nuxt.config.ts
# Check how the app config is being used in the application
echo -e "\n=== Checking for usage of appConfig or app.url ==="
rg "appConfig|useAppConfig" -A3 -B1 --type=ts --type=vue | head -50
# Check for schema.org metadata usage
echo -e "\n=== Checking for schema metadata or canonical URLs ==="
rg "schema|canonical|og:" --type=vue --type=ts -i | head -30
# Check if there are different build configurations or deployment scripts
echo -e "\n=== Checking for environment-specific build scripts ==="
cat apps/web-app/package.json | grep -A10 '"scripts"'Repository: k39space/k39
Length of output: 1266
🏁 Script executed:
#!/bin/bash
# Search for actual usage of the url key
echo "=== Searching for 'app.url' or '.url' usage ==="
rg "\.url|app\.url" --type=ts -A2 -B2 | head -60
# Search across all files (not just TS) for k39.online references
echo -e "\n=== All references to k39.online ==="
rg "k39\.online" -A1 -B1 | head -80
# Check if app.config values are actually exported and used
echo -e "\n=== Checking if app config is exported in nuxt.config ==="
grep -n "defineAppConfig\|app\.config" apps/web-app/nuxt.config.tsRepository: k39space/k39
Length of output: 5745
🏁 Script executed:
#!/bin/bash
# Get full context around the site configuration in nuxt.config.ts
echo "=== Full nuxt.config.ts site section ==="
grep -n "site:" apps/web-app/nuxt.config.ts -A10
# Check if there are any Nuxt modules that use site configuration
echo -e "\n=== Checking installed Nuxt modules ==="
grep -E "@nuxt|nuxt-" apps/web-app/package.json | head -20
# Verify if app.config.ts is actually imported/used in nuxt.config
echo -e "\n=== Checking nuxt.config.ts for app config reference ==="
head -20 apps/web-app/nuxt.config.tsRepository: k39space/k39
Length of output: 1275
Correct the hardcoded site URL to use environment-aware configuration.
The production URL is hardcoded in nuxt.config.ts under the site configuration (line 11), which is used by @nuxtjs/seo to generate sitemaps, canonical URLs, and schema.org metadata. This means all environments will emit https://k39.online in metadata, potentially causing SEO issues during development and staging.
Additionally, the unused url property in app/app.config.ts (line 3) should be removed or migrated if it serves a purpose.
Use Nuxt's environment-specific configuration overrides in nuxt.config.ts:
export default defineNuxtConfig({
site: {
url: 'https://k39.online',
name: 'Калининград Онлайн 🏙️ K39',
},
$development: {
site: {
url: 'http://localhost:3501',
},
},
$staging: {
site: {
url: 'https://staging.k39.online',
},
},
})For app.config.ts: Remove the unused url property unless it's being used elsewhere. If needed for future use, migrate it to runtimeConfig in nuxt.config.ts instead.
🤖 Prompt for AI Agents
In apps/web-app/app/app.config.ts around line 3 and nuxt.config.ts around line
11, the production site URL is hardcoded which causes all environments to emit
the same metadata; remove the unused url property from app/app.config.ts (or
migrate it into runtimeConfig if needed) and instead move site.url into
nuxt.config.ts using environment-aware overrides: set the production default in
site.url and add $development and $staging blocks that override site.url for
those environments; if any code needs the URL at runtime, expose it via
runtimeConfig rather than a hardcoded app config.
|
|
||
| useSchemaOrg([ | ||
| defineLocalBusiness({ | ||
| url: `${app.url}/${params.pageSlug}`, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the file and examine its context
fd '[pageSlug].vue' --type f | head -5Repository: k39space/k39
Length of output: 284
🏁 Script executed:
# List files in apps/web-app/app/pages directory
fd '.' apps/web-app/app/pages --type f -name '*.vue' | grep -i slugRepository: k39space/k39
Length of output: 286
🏁 Script executed:
# Find the [pageSlug].vue file
find . -name '*pageSlug*' -type f 2>/dev/nullRepository: k39space/k39
Length of output: 146
🏁 Script executed:
# Also search in the pages directory for any dynamic page files
find apps/web-app/app/pages -name '*.vue' -type f 2>/dev/null | head -20Repository: k39space/k39
Length of output: 729
🏁 Script executed:
# Search for slug-related patterns in the codebase
rg 'pageSlug|encodeURIComponent.*slug|slug.*encode' --type vue -C 3 | head -50Repository: k39space/k39
Length of output: 83
🏁 Script executed:
# Search for encodeURIComponent in Vue files
rg 'encodeURIComponent|pageSlug' apps/web-app/app --type-add 'vue:*.vue' -g '*.vue' -C 3Repository: k39space/k39
Length of output: 9454
🏁 Script executed:
# Read the specific file mentioned in the review
cat -n apps/web-app/app/pages/[pageSlug].vue | head -100Repository: k39space/k39
Length of output: 3201
🏁 Script executed:
# Search for slug validation, sanitization, or constraints
rg 'slug.*valid|slug.*sanitize|slug.*pattern|const.*slug.*=|type.*slug' apps/web-app --type ts -C 2 | head -80Repository: k39space/k39
Length of output: 860
🏁 Script executed:
# Check if there's an API endpoint that creates/validates slugs
find apps -name '*.ts' -type f -exec grep -l 'slug' {} \; | head -10Repository: k39space/k39
Length of output: 155
🏁 Script executed:
# Look for slug patterns or regex constraints
rg 'pageSlug|slug' apps/server --type ts -B 1 -A 1 | head -60Repository: k39space/k39
Length of output: 107
🏁 Script executed:
# Check the API endpoint implementation
cat -n apps/web-app/server/api/page/slug/[slug].get.tsRepository: k39space/k39
Length of output: 760
🏁 Script executed:
# Search for any slug encoding/decoding in the codebase
rg 'decodeURI|encodeURI|URLDecode|URLEncode' apps/web-app --type ts -C 2Repository: k39space/k39
Length of output: 349
🏁 Script executed:
# Check how database models define slugs (constraints, type, etc.)
find . -name '*.prisma' -o -name 'schema.*' | head -5Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Check how pages are created/edited - look for slug validation
rg 'slug.*String|slug.*required|slug.*pattern|slug.*validate' apps/database --type prisma --type ts -C 2Repository: k39space/k39
Length of output: 86
🏁 Script executed:
# Check the database schema for page slug constraints
find . -path '*/database/*' -name '*.prisma' -exec cat {} \; | grep -A 5 -B 5 'slug'Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Find prisma schema without type specification
find . -name '*.prisma' 2>/dev/nullRepository: k39space/k39
Length of output: 38
🏁 Script executed:
# Look for where the slug value comes from in Nuxt
rg 'params\.pageSlug' apps/web-app/app/pages/\[pageSlug\].vue -C 5Repository: k39space/k39
Length of output: 1215
🏁 Script executed:
# Check if pageSlug is decoded anywhere (Nuxt auto-decodes route params)
rg 'useRoute|getRouterParam' apps/web-app --type ts -A 2 | grep -A 2 'slug' | head -30Repository: k39space/k39
Length of output: 777
🏁 Script executed:
# Check if there's slug validation during page creation
rg 'slug.*[a-z0-9-]|slug.*regex|slug.*pattern' . --type ts | head -20Repository: k39space/k39
Length of output: 826
🏁 Script executed:
# Look at how Nuxt handles route params - check if there's any decode happening
rg 'decodeURIComponent|useRoute' apps/web-app/app/pages/\[pageSlug\].vueRepository: k39space/k39
Length of output: 100
🏁 Script executed:
# Check if the slug is used in other URL contexts (to understand encoding needs)
rg 'pageSlug.*http|url.*pageSlug|href.*pageSlug' apps/web-app --type vue -B 2 -A 2Repository: k39space/k39
Length of output: 83
🏁 Script executed:
# Check if there's any slug validation or constraints during page creation
rg 'slug.*create|slug.*update|POST.*page|PUT.*page' packages/database -A 3 -B 1 | head -40Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Look for where slugs are created/validated (might show character constraints)
find packages -name '*.ts' -exec grep -l 'slug' {} \; | xargs grep -l 'create\|update\|validation'Repository: k39space/k39
Length of output: 128
🏁 Script executed:
# Check navigation links to see if encodeURIComponent is used there
rg 'to:.*pageSlug|navigateTo.*pageSlug' apps/web-app/app --type vueRepository: k39space/k39
Length of output: 83
🏁 Script executed:
# Check the page repository to see if there's slug validation/sanitization during creation
cat packages/database/src/repository/page.ts | head -80Repository: k39space/k39
Length of output: 2096
🏁 Script executed:
# Check for any slug validation or transformation in services
rg 'slug.*toLowerCase|slug.*replace|slug.*transform|slug.*sanitize' packages -C 2Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Check if slugs are restricted to safe characters during creation
find packages -name '*.ts' -type f -exec grep -l 'Page\|page.*create\|page.*new' {} \; | head -5Repository: k39space/k39
Length of output: 385
🏁 Script executed:
# Check the full page table definition for any constraints
cat packages/database/src/tables.ts | grep -A 10 -B 5 'slug.*varchar'Repository: k39space/k39
Length of output: 1464
🏁 Script executed:
# Look for any form validation or slug sanitization before API calls
rg 'slug.*value|slug.*input|slug.*form' packages --type ts -B 2 -A 2 | head -40Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Check if Nuxt automatically encodes params when used in router-link 'to'
rg 'defineLocalBusiness|useSchemaOrg' apps/web-app/app -B 3 -A 3 | head -40Repository: k39space/k39
Length of output: 1392
🏁 Script executed:
# Look for any slug character restrictions or validation in frontend forms
rg 'slug.*pattern|slug.*maxlength|slug.*validation|slug.*[a-z0-9]' apps/web-app/app --type ts -C 2 | head -30Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Check if useFetch automatically encodes URL parameters
rg 'useFetch.*params|useFetch.*slug' apps/web-app/app --type ts -B 2 -A 2 | head -30Repository: k39space/k39
Length of output: 38
🏁 Script executed:
# Check if there's any slug sanitization or normalization during page creation in frontend
find apps/web-app/app -name '*.ts' -o -name '*.vue' | xargs grep -l 'slug.*sanitize\|slug.*normalize\|slug.*toLowerCase' 2>/dev/nullRepository: k39space/k39
Length of output: 40
🏁 Script executed:
# Final check: does the schema.org library handle URL encoding?
rg 'useSchemaOrg|defineLocalBusiness' -A 5 -B 5 apps/web-app/app | head -40Repository: k39space/k39
Length of output: 2383
🏁 Script executed:
# Check if there's any slug pattern validation or restriction during page creation
rg 'slug.*[a-zA-Z0-9]|slug.*pattern|slug.*regex|slug.*match' packages/database -C 2Repository: k39space/k39
Length of output: 2585
🏁 Script executed:
# Look at how Nuxt extracts route params - check if they're decoded
rg 'getRouterParam|useRoute' apps/web-app/server -B 2 -A 2 | head -20Repository: k39space/k39
Length of output: 1497
🏁 Script executed:
# Check if Nuxt's useRoute automatically decodes params
rg 'decodeURI|decode' apps/web-app/app/pages/\[pageSlug\].vueRepository: k39space/k39
Length of output: 38
🏁 Script executed:
# Verify: are slugs restricted to URL-safe characters in page creation?
find apps/web-app -name '*.ts' -o -name '*.vue' | xargs grep -l 'page.*create\|page.*POST' 2>/dev/null | head -3Repository: k39space/k39
Length of output: 237
URL-encode the pageSlug for structured data validity.
Route parameters are automatically URL-decoded by Nuxt, and when reconstructing URLs manually, encoding must be handled explicitly. Use encodeURIComponent():
- url: `${app.url}/${params.pageSlug}`,
+ url: `${app.url}/${encodeURIComponent(params.pageSlug)}`,This ensures the JSON-LD URL is properly formatted even if the slug contains special characters. Skip this only if slug creation is strictly validated to ASCII-safe characters.
📝 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.
| url: `${app.url}/${params.pageSlug}`, | |
| url: `${app.url}/${encodeURIComponent(params.pageSlug)}`, |
🤖 Prompt for AI Agents
In apps/web-app/app/pages/[pageSlug].vue around line 85, the reconstructed URL
uses params.pageSlug directly which can contain characters that break
structured-data JSON-LD; wrap the slug with encodeURIComponent when building the
URL (e.g., use encodeURIComponent(params.pageSlug)) so the generated URL is
properly percent-encoded for special characters and safe for JSON-LD output.
| defineAggregateRating({ | ||
| '@type': 'AggregateRating', | ||
| 'ratingValue': page.value?.rating, | ||
| 'ratingCount': page.value?.reviewsCount, | ||
| 'bestRating': 5, | ||
| 'worstRating': 1, | ||
| }), |
There was a problem hiding this comment.
Conditionally emit AggregateRating only when rating data exists.
The AggregateRating is currently emitted regardless of whether valid rating data exists. Schema.org requires both ratingValue and ratingCount to be present and meaningful for a valid AggregateRating. Emitting this metadata with undefined, null, or 0 values can produce invalid structured data that may harm SEO or cause search engines to ignore the metadata.
Apply this diff to conditionally include the AggregateRating:
useSchemaOrg([
defineLocalBusiness({
url: `${app.url}/${params.pageSlug}`,
name: page.value?.title,
}),
- defineAggregateRating({
- '@type': 'AggregateRating',
- 'ratingValue': page.value?.rating,
- 'ratingCount': page.value?.reviewsCount,
- 'bestRating': 5,
- 'worstRating': 1,
- }),
+ ...(page.value?.rating && page.value?.reviewsCount ? [
+ defineAggregateRating({
+ '@type': 'AggregateRating',
+ 'ratingValue': page.value.rating,
+ 'ratingCount': page.value.reviewsCount,
+ 'bestRating': 5,
+ 'worstRating': 1,
+ }),
+ ] : []),
])🤖 Prompt for AI Agents
In apps/web-app/app/pages/[pageSlug].vue around lines 88-94, the AggregateRating
JSON-LD is always emitted even when page.value?.rating or
page.value?.reviewsCount are missing or zero; update the template to only
include/emit defineAggregateRating when both ratingValue and ratingCount are
present and meaningful (e.g., ratingValue != null/undefined and ratingCount >
0). Implement a conditional guard around the defineAggregateRating call (or
build the structured-data array conditionally) so AggregateRating is omitted
when the required values are absent or invalid.
Summary by cubic
Adds schema.org structured data for reviews, authors, and business pages to improve SEO and enable rich snippets.
Written for commit 5858cdd. Summary will update automatically on new commits.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.