Skip to content

chore: schema on review#59

Merged
hmbanan666 merged 3 commits intomainfrom
review-rework
Dec 16, 2025
Merged

chore: schema on review#59
hmbanan666 merged 3 commits intomainfrom
review-rework

Conversation

@hmbanan666
Copy link
Contributor

@hmbanan666 hmbanan666 commented Dec 15, 2025


Summary by cubic

Adds schema.org structured data for reviews, authors, and business pages to improve SEO and enable rich snippets.

  • New Features
    • ReviewCard: Annotated as Review with rating (best/worst), headline, body, pros/cons, datePublished, author (Person with name, image, profile url), and like/dislike interaction stats.
    • ProfileCard: Annotated author as Person with name, image, and profile url.
    • Page: Added LocalBusiness with url and name, and AggregateRating with ratingValue, ratingCount, bestRating, and worstRating.

Written for commit 5858cdd. Summary will update automatically on new commits.

Summary by CodeRabbit

  • Chores
    • Enhanced structured metadata across review cards, user profiles, and pages using schema.org standards for improved search engine visibility and data interoperability.
    • Updated datetime formatting to use ISO 8601 standard for review timestamps.
    • Added application URL configuration to support metadata generation.

✏️ Tip: You can customize this high-level summary in your review settings.

@hmbanan666 hmbanan666 self-assigned this Dec 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Walkthrough

This 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

Cohort / File(s) Change Summary
Schema.org metadata integration
apps/web-app/app/components/page/ReviewCard.vue, apps/web-app/app/components/user/ProfileCard.vue, apps/web-app/app/pages/[pageSlug].vue
Adds useSchemaOrg() composable calls to emit structured metadata: ReviewCard now exports Review schema with rating, headline, author, and interaction statistics; ProfileCard exports Person schema; [pageSlug].vue exports LocalBusiness and AggregateRating schemas. Also adds useAppConfig() calls to access app URL for structured data. ReviewCard replaces time element datetime with ISO format.
App configuration
apps/web-app/app/app.config.ts
Adds new url property ('https://k39.online') to app config object for use in schema.org metadata generation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12–18 minutes

  • Repetitive pattern of useSchemaOrg() and useAppConfig() additions across 3 components makes many changes predictable
  • App config change is trivial
  • Verification needed: schema.org field mappings accuracy, ISO datetime formatting correctness, and proper data flow from component data to schema structures
  • Low business logic density; changes are mostly declarative metadata annotations

Possibly related PRs

Poem

🐰 Hopping through markup, we weave schemas with care,
Review and Person now dance in the air,
LocalBusiness blooms where pages unfold,
With dates in ISO and metadata bold,
SEO springs forth—oh, what structured delight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'chore: schema on review' is vague and does not accurately reflect the scope of changes. The PR adds schema.org microdata across multiple files (ReviewCard, ProfileCard, app.config, and the reviews page), but the title only mentions 'review' and uses vague phrasing ('schema on review') that doesn't convey the full intent or breadth of the changes. Revise the title to be more descriptive, such as 'feat: add schema.org microdata for reviews and authors' or 'feat: implement schema.org structured data for SEO enhancement', which better captures that this PR adds semantic markup across multiple components for SEO and rich snippets.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch review-rework

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 2 files

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/web-app/app/components/user/ProfileCard.vue (2)

2-7: Align schema.org Person URL with https and with ReviewCard

The root element uses itemtype="http://schema.org/Person" while the review card uses https://schema.org/Review. Consider switching this to https://schema.org/Person for consistency and to follow current schema.org/SEO recommendations.


17-22: Image/name microdata looks good; minor cleanup possible

The itemprop="image" + itemprop="name" usage is semantically correct. The expression user.avatarUrl ?? undefined is a bit redundant, as passing null/undefined to 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 scale

The reviewRating block is structured well, but:

  • You’re using itemtype="http://schema.org/Rating" here while the root uses https://schema.org/Review. Consider switching this to https://schema.org/Rating for consistency.
  • You hard‑code bestRating="5" and worstRating="1". That’s fine if your domain guarantees a 1–5 scale; if 0 is ever valid, this would be misleading, so worth double‑checking.

87-92: Consider including pros/cons in the reviewBody for richer structured data

Currently only the “Комментарий” section is tagged with itemprop="reviewBody", while “Достоинства” and “Недостатки” are left unannotated. For search engines, reviewBody is 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 single reviewBody container or otherwise combined.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75887aa and 19f0a72.

📒 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.createdAt is configured in the database schema with Drizzle's mode: 'string', which means it's returned from the database as an ISO 8601 string—not a Date object. Therefore, :datetime="review.createdAt" is already machine-readable and consistent with the datePublished meta tag. No changes are needed.

Likely an incorrect or invalid review comment.

Comment on lines +2 to +7
<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"
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's read the full ReviewCard.vue file
cat -n apps/web-app/app/components/page/ReviewCard.vue

Repository: k39space/k39

Length of output: 5425


🏁 Script executed:

# Search for files that import or use ReviewCard
rg "ReviewCard" --type vue --type js --type ts -l

Repository: 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 50

Repository: k39space/k39

Length of output: 949


🏁 Script executed:

# Look for files importing ReviewCard
fd "ReviewCard" --type f

Repository: k39space/k39

Length of output: 264


🏁 Script executed:

cat -n apps/web-app/app/pages/[pageSlug]/reviews.vue

Repository: 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 20

Repository: 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 -10

Repository: 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 -100

Repository: 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).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/web-app/app/components/page/ReviewCard.vue (1)

2-7: Remove itemprop="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 only itemscope and itemtype.

  <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

📥 Commits

Reviewing files that changed from the base of the PR and between 19f0a72 and 7c7fd41.

📒 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 reviewRating with bestRating, worstRating, and ratingValue follows 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-if ensures no empty metadata is emitted.


102-107: Date handling is correct.

Using yyyy-MM-dd format for datePublished meta 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 InteractionCounter items correctly specify LikeAction and DislikeAction with their respective counts. Using .toString() ensures content attributes receive string values.

Comment on lines +115 to +128
<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>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

itemReviewed is conditionally rendered and breaks grid layout.

Two issues:

  1. Semantic issue: itemReviewed only appears when review.photos.length > 0. Every Review should declare what's being reviewed, regardless of photos.

  2. 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).

Comment on lines +6 to +15
<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>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

@hmbanan666 hmbanan666 merged commit b216a5f into main Dec 16, 2025
1 of 2 checks passed
@hmbanan666 hmbanan666 deleted the review-rework branch December 16, 2025 08:30
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c7fd41 and 5858cdd.

📒 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',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for the app.config.ts file
find . -type f -name "app.config.ts" | head -20

Repository: 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=ts

Repository: 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.ts

Repository: 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.ts

Repository: 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}`,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the file and examine its context
fd '[pageSlug].vue' --type f | head -5

Repository: 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 slug

Repository: k39space/k39

Length of output: 286


🏁 Script executed:

# Find the [pageSlug].vue file
find . -name '*pageSlug*' -type f 2>/dev/null

Repository: 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 -20

Repository: 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 -50

Repository: 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 3

Repository: 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 -100

Repository: 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 -80

Repository: 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 -10

Repository: 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 -60

Repository: k39space/k39

Length of output: 107


🏁 Script executed:

# Check the API endpoint implementation
cat -n apps/web-app/server/api/page/slug/[slug].get.ts

Repository: 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 2

Repository: k39space/k39

Length of output: 349


🏁 Script executed:

# Check how database models define slugs (constraints, type, etc.)
find . -name '*.prisma' -o -name 'schema.*' | head -5

Repository: 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 2

Repository: 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/null

Repository: 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 5

Repository: 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 -30

Repository: 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 -20

Repository: 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\].vue

Repository: 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 2

Repository: 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 -40

Repository: 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 vue

Repository: 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 -80

Repository: 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 2

Repository: 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 -5

Repository: 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 -40

Repository: 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 -40

Repository: 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 -30

Repository: 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 -30

Repository: 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/null

Repository: 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 -40

Repository: 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 2

Repository: 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 -20

Repository: 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\].vue

Repository: 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 -3

Repository: 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.

Suggested change
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.

Comment on lines +88 to +94
defineAggregateRating({
'@type': 'AggregateRating',
'ratingValue': page.value?.rating,
'ratingCount': page.value?.reviewsCount,
'bestRating': 5,
'worstRating': 1,
}),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant