Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web-app/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -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.

githubUrl: 'https://github.com/k39space/k39',
},
ui: {
Expand Down
39 changes: 38 additions & 1 deletion apps/web-app/app/components/page/ReviewCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
:content="review.comment"
/>

<time :datetime="review.createdAt" class="text-sm/5 text-muted italic">
<time :datetime="new Date(review.createdAt).toISOString()" class="text-sm/5 text-muted italic">
Опубликовано {{ format(review.createdAt, 'dd MMMM yyyy', { locale: ru }) }}
</time>

Expand Down Expand Up @@ -126,4 +126,41 @@ const { review } = defineProps<{ review: PageReviewWithData, updateData: () => v
function copyReviewUrlToClipboard() {
navigator.clipboard.writeText(`${window.location.origin}/review/${review.id}`)
}

const { app } = useAppConfig()

useSchemaOrg([
defineReview({
'@type': 'Review',
'reviewRating': {
'@type': 'Rating',
'bestRating': 5,
'worstRating': 1,
'ratingValue': review.rating,
},
'headline': review.comment?.slice(0, 100),
'reviewBody': review.comment,
'positiveNotes': review.pros,
'negativeNotes': review.cons,
'datePublished': format(review.createdAt, 'yyyy-MM-dd'),
'author': {
'@type': 'Person',
'name': review.user.name,
'image': review.user.avatarUrl,
'url': `${app.url}/u/${review.user.username}`,
},
'interactionStatistic': [
{
'@type': 'InteractionCounter',
'interactionType': 'https://schema.org/LikeAction',
'userInteractionCount': review.likesCount,
},
{
'@type': 'InteractionCounter',
'interactionType': 'https://schema.org/DislikeAction',
'userInteractionCount': review.dislikesCount,
},
],
}),
])
</script>
10 changes: 10 additions & 0 deletions apps/web-app/app/components/user/ProfileCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ import { getUserXpPercent } from '#shared/utils/user'
const { user } = defineProps<{ user: User }>()

const progressPercent = computed(() => getUserXpPercent(user.xp, user.xpToNextLevel))

const { app } = useAppConfig()

useSchemaOrg([
definePerson({
name: user.name,
image: user.avatarUrl,
url: `${app.url}/u/${user.username}`,
}),
])
</script>
15 changes: 15 additions & 0 deletions apps/web-app/app/pages/[pageSlug].vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function updateData() {
}

const { items } = useBreadcrumb()
const { app } = useAppConfig()

const submenuItems = computed<NavigationMenuItem[]>(() => [
{
Expand All @@ -78,4 +79,18 @@ const submenuItems = computed<NavigationMenuItem[]>(() => [
badge: page.value?.points?.length ? page.value.points.length : undefined, // show if more than 0
},
])

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.

name: page.value?.title,
}),
defineAggregateRating({
'@type': 'AggregateRating',
'ratingValue': page.value?.rating,
'ratingCount': page.value?.reviewsCount,
'bestRating': 5,
'worstRating': 1,
}),
Comment on lines +88 to +94
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.

])
</script>