feat(seo): add comprehensive SEO engine#37
Conversation
… per-page components - Add src/components/SEO/ directory with complete SEO engine - seo.config.js: site identity, domain, geo, social handles - schemas.js: 7 JSON-LD builders (WebSite, Organization, Event, Breadcrumb, FAQ, ImageGallery) - SEOHead.js: core Head component with OG, Twitter Cards, AI/LLM meta - 10 per-page SEO components (HomeSEO, AboutSEO, EventsSEO, etc.) - Add public/robots.txt with crawler directives and sitemap reference - Add src/pages/sitemap.xml.js with dynamic event sitemap generation - Add SSR data fetching to events/[id] for SEO-friendly event pages - Update _app.js: remove duplicate OG tags, keep only PWA meta - Update _document.js: add geo tags, author, publisher meta - Integrate SEO into: home, events, event detail, team, gallery, cart, checkout pages
📝 WalkthroughWalkthroughAdds a full SEO system: robots.txt, a reusable SEOHead, per-page SEO wrappers, JSON‑LD schema builders, centralized SITE config, sitemap.xml SSR generator, and integration of SEO components across pages with server-side event fetching for event detail SEO. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client (crawler/request)
participant S as Next.js Server
participant DB as Database
participant FS as File System
C->>S: GET /sitemap.xml
S->>DB: fetch published events (paginated)
alt DB available
DB-->>S: events pages
else DB error or unreachable
S->>FS: read static routes only
end
S->>S: build XML (static + event entries)
S->>C: 200 application/xml (sitemap) + cache headers
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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.
Pull request overview
Adds a reusable SEO subsystem (meta/OG/Twitter/JSON-LD) and crawl infrastructure (robots + dynamic sitemap) to improve indexability of public pages in the Next.js Pages Router app.
Changes:
- Introduces
src/components/SEO(site config,SEOHead, JSON-LD schema builders, and per-route SEO wrappers). - Adds
public/robots.txtandsrc/pages/sitemap.xml.jsto expose crawl directives and an event-aware sitemap. - Integrates SEO components into key public and transactional pages and updates
_app.js/_document.jshead metadata strategy.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| src/components/SEO/seo.config.js | Central SEO site identity + canonical URL helper. |
| src/components/SEO/schemas.js | JSON-LD builders (WebSite/Organization/Event/Breadcrumb/FAQ/ImageGallery). |
| src/components/SEO/SEOHead.js | Core <Head> renderer for meta/OG/Twitter + JSON-LD injection. |
| src/components/SEO/index.js | Barrel exports for per-page SEO components + config exports. |
| src/components/SEO/pages/HomeSEO.js | Home page SEO wrapper using schemas. |
| src/components/SEO/pages/AboutSEO.js | About page SEO wrapper (currently added but not wired in pages). |
| src/components/SEO/pages/EventsSEO.js | Events listing SEO wrapper. |
| src/components/SEO/pages/EventDetailSEO.js | Event detail SEO wrapper (expects server-provided event fields). |
| src/components/SEO/pages/GallerySEO.js | Gallery SEO wrapper. |
| src/components/SEO/pages/TeamSEO.js | Team SEO wrapper. |
| src/components/SEO/pages/CartSEO.js | Cart noindex SEO wrapper. |
| src/components/SEO/pages/CheckoutSEO.js | Checkout noindex SEO wrapper. |
| src/components/SEO/pages/LoginSEO.js | Login noindex SEO wrapper (currently added but not wired in pages). |
| src/components/SEO/pages/RegisterSEO.js | Register SEO wrapper (currently added but not wired in pages). |
| src/pages/_app.js | Removes prior global OG/title/description tags and keeps only PWA meta. |
| src/pages/_document.js | Adds document-level geo/author/publisher meta tags. |
| src/pages/sitemap.xml.js | Adds dynamic sitemap endpoint intended to include published events. |
| public/robots.txt | Adds crawler rules + sitemap reference. |
| src/pages/index.js | Mounts <HomeSEO /> on the home route. |
| src/pages/events/index.js | Mounts <EventsSEO /> on the events listing route. |
| src/pages/events/[id].js | Adds SSR data fetch for SEO + mounts <EventDetailSEO />. |
| src/pages/gallery/index.js | Mounts <GallerySEO /> on the gallery route. |
| src/pages/team/index.js | Mounts <TeamSEO /> on the team route. |
| src/pages/cart/index.js | Mounts <CartSEO /> on the cart route. |
| src/pages/checkout/index.js | Mounts <CheckoutSEO /> on the checkout route. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Fetch all published event IDs | ||
| let events = [] | ||
| try { | ||
| events = await eventService.getEvents({ limit: 500, offset: 0 }) |
There was a problem hiding this comment.
eventService does not expose getEvents() (the public service has getPublishedEvents() and getEventById()). As written, sitemap generation will throw and always fall back to the static-only sitemap. Update this to call an existing method or add a sitemap-specific service method that returns the needed fields (id + updated_at).
| // Fetch all published event IDs | |
| let events = [] | |
| try { | |
| events = await eventService.getEvents({ limit: 500, offset: 0 }) | |
| // Fetch all published events | |
| let events = [] | |
| try { | |
| events = await eventService.getPublishedEvents({ limit: 500, offset: 0 }) |
| lastmod: event.updated_at | ||
| ? new Date(event.updated_at).toISOString().slice(0, 10) |
There was a problem hiding this comment.
The sitemap lastmod uses event.updated_at, but the public event queries currently return created_at (and not updated_at) and also alias name as title. Ensure the data source used here actually includes updated_at (or adjust lastmod to a field that exists) so <lastmod> is correct.
| lastmod: event.updated_at | |
| ? new Date(event.updated_at).toISOString().slice(0, 10) | |
| lastmod: event.created_at | |
| ? new Date(event.created_at).toISOString().slice(0, 10) |
| if (!id || isNaN(parseInt(id))) return { props: { seoEvent: null } } | ||
|
|
||
| // Import server-side service directly — avoids an extra HTTP round-trip | ||
| const eventService = (await import('src/services/event-service')).default | ||
| const event = await eventService.getEventById(parseInt(id)) |
There was a problem hiding this comment.
parseInt will accept strings like "123abc" and treat them as valid IDs, so /events/123abc would fetch event 123. Consider using Number(id) + Number.isInteger(...) (and optionally verifying String(eventId) === id) to ensure the entire route param is a valid integer.
| if (!id || isNaN(parseInt(id))) return { props: { seoEvent: null } } | |
| // Import server-side service directly — avoids an extra HTTP round-trip | |
| const eventService = (await import('src/services/event-service')).default | |
| const event = await eventService.getEventById(parseInt(id)) | |
| const eventId = Number(id) | |
| if (!Number.isInteger(eventId) || String(eventId) !== id) { | |
| return { props: { seoEvent: null } } | |
| } | |
| // Import server-side service directly — avoids an extra HTTP round-trip | |
| const eventService = (await import('src/services/event-service')).default | |
| const event = await eventService.getEventById(eventId) |
| id: event.id ?? null, | ||
| name: event.name ?? null, | ||
| tagline: event.tagline ?? null, | ||
| description: event.description ?? null, | ||
| venue: event.venue ?? null, | ||
| ticket_price: event.ticket_price ?? null, | ||
| banner_url: event.banner_url ?? null, | ||
| start_time: event.start_time ? String(event.start_time) : null, | ||
| end_time: event.end_time ? String(event.end_time) : null, | ||
| department_name: event.department_name ?? null, |
There was a problem hiding this comment.
eventService.getEventById() returns a shape with title and departmentName (and does not provide name, department_name, or banner_url). The current seoEvent serialization will therefore populate mostly null/undefined, and EventDetailSEO/eventSchema() won't get the data they expect. Map the returned fields to the SEO schema contract (or adjust the SEO schema/components to the service’s field names) so event title/department/image render correctly.
| id: event.id ?? null, | |
| name: event.name ?? null, | |
| tagline: event.tagline ?? null, | |
| description: event.description ?? null, | |
| venue: event.venue ?? null, | |
| ticket_price: event.ticket_price ?? null, | |
| banner_url: event.banner_url ?? null, | |
| start_time: event.start_time ? String(event.start_time) : null, | |
| end_time: event.end_time ? String(event.end_time) : null, | |
| department_name: event.department_name ?? null, | |
| id: event.id ?? null, | |
| name: event.name ?? event.title ?? null, | |
| tagline: event.tagline ?? null, | |
| description: event.description ?? null, | |
| venue: event.venue ?? null, | |
| ticket_price: event.ticket_price ?? null, | |
| banner_url: event.banner_url ?? null, | |
| start_time: event.start_time ? String(event.start_time) : null, | |
| end_time: event.end_time ? String(event.end_time) : null, | |
| department_name: event.department_name ?? event.departmentName ?? null, |
| {schemas.map((schema, i) => ( | ||
| <script | ||
| key={`ld-${i}`} | ||
| type='application/ld+json' | ||
| dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} | ||
| /> |
There was a problem hiding this comment.
dangerouslySetInnerHTML is injecting JSON.stringify(schema) directly into a <script> tag. If any schema value contains </script> or <, it can break out of the script context (JSON-LD XSS vector). Escape < (e.g., replace with \u003c) or use a safe serializer before injecting JSON-LD.
| <Head> | ||
| <title>{themeConfig.templateName}</title> | ||
| <meta name='description' content='Citronics — The official college event management platform' /> | ||
| {/* | ||
| * Global fallback <Head> — individual pages override these via <SEOHead>. | ||
| * Only meta that must be present on every single render lives here. | ||
| */} | ||
| <meta name='viewport' content='initial-scale=1, width=device-width' /> | ||
|
|
||
| {/* PWA meta */} | ||
| <meta name='application-name' content={themeConfig.templateName} /> | ||
| <meta name='application-name' content='Citronics 2026' /> | ||
| <meta name='apple-mobile-web-app-capable' content='yes' /> | ||
| <meta name='apple-mobile-web-app-status-bar-style' content='default' /> | ||
| <meta name='apple-mobile-web-app-title' content={themeConfig.templateName} /> | ||
| <meta name='apple-mobile-web-app-title' content='Citronics 2026' /> | ||
| <meta name='format-detection' content='telephone=no' /> | ||
| <meta name='mobile-web-app-capable' content='yes' /> | ||
| <meta name='theme-color' content='#7C3AED' /> | ||
| <link rel='manifest' href='/manifest.json' /> | ||
|
|
||
| {/* Open Graph */} | ||
| <meta property='og:type' content='website' /> | ||
| <meta property='og:title' content={themeConfig.templateName} /> | ||
| <meta property='og:description' content='Citronics — The official college event management platform' /> | ||
| <meta property='og:image' content='/images/icons/pwa/icon-512x512.png' /> | ||
| </Head> |
There was a problem hiding this comment.
Removing the global <title> and meta description means any page that doesn't render an SEO component will now have no title/description at all. A quick repo search shows no usage of AboutSEO/LoginSEO/RegisterSEO in src/pages/* yet, so at least /about, /login, and /register will lose these tags. Either keep a minimal default title/description here, or ensure every route mounts the appropriate SEO component.
| */ | ||
|
|
||
| import eventService from 'src/services/event-service' | ||
| import { SITE } from 'src/components/SEO' |
There was a problem hiding this comment.
Server-side pages like sitemap.xml.js only need SITE.url, but importing { SITE } from the src/components/SEO barrel also pulls in per-page React components. Prefer importing from src/components/SEO/seo.config to keep the server route decoupled from the component barrel and reduce the chance of accidental client-only dependencies being bundled.
| import { SITE } from 'src/components/SEO' | |
| import { SITE } from 'src/components/SEO/seo.config' |
| const HomeSEO = () => ( | ||
| <SEOHead | ||
| title='Citronics 2026 — CDGI Annual Tech Fest | April 7–9 | 35+ Events, 5000+ Participants' | ||
| description='Citronics 2026 is Central India`"`s largest annual college tech fest hosted by Chameli Devi Group of Institutions (CDGI), Indore. Featuring 35+ competitions in AI, robotics, coding, management & cultural events — April 7 to 9, 2026. Register now and compete!' |
There was a problem hiding this comment.
The description contains a broken quote escape sequence (Central India"s) which will render literally in the meta description. Use a normal apostrophe (or proper JS escaping) so it becomes "Central India's".
| description='Citronics 2026 is Central India`"`s largest annual college tech fest hosted by Chameli Devi Group of Institutions (CDGI), Indore. Featuring 35+ competitions in AI, robotics, coding, management & cultural events — April 7 to 9, 2026. Register now and compete!' | |
| description='Citronics 2026 is Central India\'s largest annual college tech fest hosted by Chameli Devi Group of Institutions (CDGI), Indore. Featuring 35+ competitions in AI, robotics, coding, management & cultural events — April 7 to 9, 2026. Register now and compete!' |
| const TeamSEO = () => ( | ||
| <SEOHead | ||
| title='Core Team — Citronics 2026 | Meet the Organizers & Student Council | CDGI Indore' | ||
| description='Meet the student organisers, department heads, and faculty mentors behind Citronics 2026 at CDGI, Indore. The dedicated team of coordinators, technical leads, and operations crew making Central India`"`s largest tech fest a reality.' |
There was a problem hiding this comment.
The description contains a broken quote escape sequence (Central India"s) which will render literally in the meta description. Use a normal apostrophe (or proper JS escaping) so it becomes "Central India's".
| description='Meet the student organisers, department heads, and faculty mentors behind Citronics 2026 at CDGI, Indore. The dedicated team of coordinators, technical leads, and operations crew making Central India`"`s largest tech fest a reality.' | |
| description="Meet the student organisers, department heads, and faculty mentors behind Citronics 2026 at CDGI, Indore. The dedicated team of coordinators, technical leads, and operations crew making Central India's largest tech fest a reality." |
| start_time: event.start_time ? String(event.start_time) : null, | ||
| end_time: event.end_time ? String(event.end_time) : null, |
There was a problem hiding this comment.
start_time/end_time are coerced with String(...), which will produce a non-ISO human string when the value is a Date object (e.g., "Sat Mar 07 2026 ..."). JSON-LD expects ISO 8601 timestamps; prefer toISOString() (or leave Date objects un-stringified and let Next.js serialize them appropriately) so structured data is valid.
| start_time: event.start_time ? String(event.start_time) : null, | |
| end_time: event.end_time ? String(event.end_time) : null, | |
| start_time: event.start_time | |
| ? (event.start_time instanceof Date | |
| ? event.start_time.toISOString() | |
| : event.start_time) | |
| : null, | |
| end_time: event.end_time | |
| ? (event.end_time instanceof Date | |
| ? event.end_time.toISOString() | |
| : event.end_time) | |
| : null, |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (2)
src/pages/sitemap.xml.js (1)
54-57: Avoid hard-capping the sitemap to the first 500 events.This works for the current fest size, but it will silently truncate
/sitemap.xmlonce the event count grows past one page. Consider paging throughgetEventsuntil exhaustion, or switching to a sitemap index if you want this endpoint to stay comprehensive.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/sitemap.xml.js` around lines 54 - 57, The sitemap currently hard-caps events by calling eventService.getEvents({ limit: 500, offset: 0 }) which will silently truncate results once there are >500 events; change sitemap generation to page through eventService.getEvents using limit/offset (or cursor if supported) in a loop — calling getEvents repeatedly and incrementing offset until an empty page is returned — and concatenate all event IDs before rendering the sitemap (or alternatively implement a sitemap index split into pages), updating the code in sitemap.xml.js where getEvents is called.src/components/SEO/schemas.js (1)
55-55: Move year-specific SEO literals intoseo.config.Dates, counts, and offer windows are duplicated as hardcoded strings across multiple builders. This will drift on the next event rollover and leave stale JSON-LD even if the main site config is updated. Centralizing these values in
SITEwould make the builders much safer to reuse.Also applies to: 119-120, 143-147, 138-139, 197-197
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SEO/schemas.js` at line 55, Replace hardcoded year/count/date/venue strings in the JSON-LD builders with centralized keys on the SEO config (e.g., add properties to seo.config / SITE such as eventYear, eventDates, competitionCount, participantCount, eventVenue, offerWindow) and update the description line and other occurrences in src/components/SEO/schemas.js (where SITE.tagline is used) to interpolate those SITE.seo.* values instead of literal strings; ensure all referenced builders at the noted positions (lines ~119-120, 138-147, 197) pull from the new seo config keys and update any tests or docs that expect the old literals.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@public/robots.txt`:
- Around line 15-16: Remove the Disallow directives that prevent crawlers from
fetching the cart and checkout pages so their page-level SEO components can be
read; specifically update public/robots.txt to delete or comment out the lines
"Disallow: /cart" and "Disallow: /checkout" (or conditionally allow those paths
for bots) so the meta/robots/canonical tags emitted by src/pages/cart/index.js
and src/pages/checkout/index.js are accessible to crawlers.
- Line 23: Replace the current blanket "Disallow: /_next/" rule in robots.txt
with scoped rules so crawlers can fetch render-critical assets: keep "Disallow:
/_next/" but add "Allow: /_next/static/" and explicitly "Disallow: /_next/image"
(i.e., use the patterns Allow: /_next/static/ and Disallow: /_next/image
alongside the existing Disallow: /_next/ to permit JS/CSS bundles while blocking
the image optimizer).
In `@src/components/SEO/pages/GallerySEO.js`:
- Around line 12-14: The meta description string in GallerySEO.js contains a
broken escaped sequence (`India\`"\`s`) that will render verbatim; update
the description value (the description prop/variable in this component) to use a
proper apostrophe (e.g., "India's" or a typographic ’) so the meta description
reads correctly (replace the malformed `India\`"\`s` with "India's").
In `@src/components/SEO/pages/HomeSEO.js`:
- Line 18: The meta description in HomeSEO.js (the description prop string)
contains the mangled sequence "`"`" instead of an apostrophe; update the
description value used in the HomeSEO component to replace that sequence with a
proper apostrophe character (') so the string reads "Central India's largest..."
(or use the HTML entity ' if you prefer) to ensure crawlers and social
previews show correct copy.
In `@src/components/SEO/pages/RegisterSEO.js`:
- Around line 12-14: The meta description string in RegisterSEO.js contains an
escaped HTML entity artifact ("India\`"\`s") which will render incorrectly;
update the description value (the description property in the RegisterSEO
component) to use a proper apostrophe so it reads "India's" (e.g., replace the
malformed sequence with a plain single quote or the correct HTML entity) and
ensure the rest of the punctuation (— April 7–9, 2026) remains unchanged.
In `@src/components/SEO/pages/TeamSEO.js`:
- Around line 12-14: In TeamSEO.js fix the HTML-escaped apostrophe in the
description string so the meta description reads "India's" instead of the
literal backtick/quote sequence; locate the description prop (the
description='Meet the student organisers... Central India`"`s largest tech
fest a reality.') and replace the escaped sequence with a normal apostrophe (')
or the proper HTML entity (' or ') so the rendered meta description
shows "India's".
In `@src/components/SEO/schemas.js`:
- Around line 132-142: The fest-level schema currently has a contradictory
offers object (offers with price: '0', url: `${SITE.url}/events`, validFrom,
etc.) while isAccessibleForFree is false and the content mentions both free and
paid sub-events; remove the entire fest-level offers block (the offers property
and its fields) and ensure isAccessibleForFree correctly reflects whether the
festival as a whole is free, then move per-ticket pricing into individual Event
schemas (create separate Event items each with their own offers) rather than
using the top-level offers or AggregateOffer.
In `@src/components/SEO/SEOHead.js`:
- Around line 79-85: The JSON-LD injection uses JSON.stringify(schema) directly
in dangerouslySetInnerHTML (inside the schemas.map in SEOHead.js), which allows
strings like </script> to break out and enable XSS; fix by escaping the
serialized JSON before injecting (e.g., replace problematic characters/sequences
such as '<', '</', or specifically '</script>' with their safe escapes like
'\\u003c' or '\\u003c/' ) and use that escaped string in dangerouslySetInnerHTML
(update the map callback that creates the <script> to compute escapedJson =
JSON.stringify(schema).replace(... ) and pass { __html: escapedJson } instead).
In `@src/pages/_app.js`:
- Around line 97-100: Pages about/index.js, login/index.js, and
register/index.js are missing their respective SEO components and several routes
(tickets/index.js, tickets/verify.js, checkout/payment-status.js) have no SEO
coverage while _app.js only provides minimal global Head metadata; import and
render each page's corresponding SEO component (e.g., AboutSEO, LoginSEO,
RegisterSEO) at the top of those page components so title/description/OG tags
are present, and for tickets/* and checkout/payment-status either (A) add new
page-specific SEO components (e.g., TicketsSEO, TicketVerifySEO,
PaymentStatusSEO) and include them in the pages or (B) if omission is
intentional, update the comment near the global <Head> in _app.js to explicitly
list and justify excluded routes; ensure you reference the page component names
(about/index.js, login/index.js, register/index.js, tickets/index.js,
tickets/verify.js, checkout/payment-status.js) when making the changes.
In `@src/pages/events/`[id].js:
- Around line 41-52: The seoEvent mapping uses fields that getEventById doesn't
return; update the properties to match the service aliases so EventDetailSEO
receives real values: use event.title ?? null for the name/title,
event.departmentName ?? null for department_name, and event.bannerUrl ?? null
for banner_url (keep the start/end time conversions as-is but verify field names
if getEventById uses startTime/endTime). Replace the current seoEvent keys
(e.g., name, department_name, banner_url) to pull from event.title,
event.departmentName, and event.bannerUrl so EventDetailSEO gets the per-event
title, department and OG image.
In `@src/pages/index.js`:
- Line 62: The HomeSEO component is currently rendered only in the success
branch after fetchHomeData() guards, causing title/canonical/robots tags to be
missing during loading/error states; move the <HomeSEO /> render out of the
success-only branch so it appears before the loading/error early returns (or
alternatively include <HomeSEO /> in each early return path) — update the JSX in
the component that calls fetchHomeData() to render HomeSEO at the top-level
(above the loading/error conditionals) so the SEO metadata is always present.
---
Nitpick comments:
In `@src/components/SEO/schemas.js`:
- Line 55: Replace hardcoded year/count/date/venue strings in the JSON-LD
builders with centralized keys on the SEO config (e.g., add properties to
seo.config / SITE such as eventYear, eventDates, competitionCount,
participantCount, eventVenue, offerWindow) and update the description line and
other occurrences in src/components/SEO/schemas.js (where SITE.tagline is used)
to interpolate those SITE.seo.* values instead of literal strings; ensure all
referenced builders at the noted positions (lines ~119-120, 138-147, 197) pull
from the new seo config keys and update any tests or docs that expect the old
literals.
In `@src/pages/sitemap.xml.js`:
- Around line 54-57: The sitemap currently hard-caps events by calling
eventService.getEvents({ limit: 500, offset: 0 }) which will silently truncate
results once there are >500 events; change sitemap generation to page through
eventService.getEvents using limit/offset (or cursor if supported) in a loop —
calling getEvents repeatedly and incrementing offset until an empty page is
returned — and concatenate all event IDs before rendering the sitemap (or
alternatively implement a sitemap index split into pages), updating the code in
sitemap.xml.js where getEvents is called.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 79ddfa29-2dc8-4e4d-b3c2-c26bb7e29a14
📒 Files selected for processing (25)
public/robots.txtsrc/components/SEO/SEOHead.jssrc/components/SEO/index.jssrc/components/SEO/pages/AboutSEO.jssrc/components/SEO/pages/CartSEO.jssrc/components/SEO/pages/CheckoutSEO.jssrc/components/SEO/pages/EventDetailSEO.jssrc/components/SEO/pages/EventsSEO.jssrc/components/SEO/pages/GallerySEO.jssrc/components/SEO/pages/HomeSEO.jssrc/components/SEO/pages/LoginSEO.jssrc/components/SEO/pages/RegisterSEO.jssrc/components/SEO/pages/TeamSEO.jssrc/components/SEO/schemas.jssrc/components/SEO/seo.config.jssrc/pages/_app.jssrc/pages/_document.jssrc/pages/cart/index.jssrc/pages/checkout/index.jssrc/pages/events/[id].jssrc/pages/events/index.jssrc/pages/gallery/index.jssrc/pages/index.jssrc/pages/sitemap.xml.jssrc/pages/team/index.js
| {/* | ||
| * Global fallback <Head> — individual pages override these via <SEOHead>. | ||
| * Only meta that must be present on every single render lives here. | ||
| */} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the src/pages/_app.js file around lines 97-100
cat -n src/pages/_app.js | sed -n '90,110p'Repository: NexEvent/Citronics_2k26
Length of output: 1193
🏁 Script executed:
# Check the overall structure of src/pages/_app.js
wc -l src/pages/_app.jsRepository: NexEvent/Citronics_2k26
Length of output: 88
🏁 Script executed:
# Find all page files in the pages directory
find src/pages -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.tsx" \) | grep -v _app | grep -v _document | sortRepository: NexEvent/Citronics_2k26
Length of output: 1383
🏁 Script executed:
# Search for SEOHead usage across the codebase
rg -l "SEOHead" --type js --type jsx --type tsxRepository: NexEvent/Citronics_2k26
Length of output: 94
🏁 Script executed:
# Search for SEOHead usage across the codebase
rg "SEOHead" --type-list | head -20Repository: NexEvent/Citronics_2k26
Length of output: 456
🏁 Script executed:
# Simpler search for SEOHead
rg "SEOHead"Repository: NexEvent/Citronics_2k26
Length of output: 1863
🏁 Script executed:
# Check for SEOHead component definition
rg "export.*SEOHead\|function SEOHead\|const SEOHead" -A 3Repository: NexEvent/Citronics_2k26
Length of output: 49
🏁 Script executed:
# Sample check key pages for metadata setup
for page in "src/pages/index.js" "src/pages/events/index.js" "src/pages/about/index.js" "src/pages/dashboard/index.js"; do
echo "=== $page ===";
if [ -f "$page" ]; then
head -50 "$page" | rg "Head|SEO|meta|title|description" -i;
fi;
doneRepository: NexEvent/Citronics_2k26
Length of output: 503
🏁 Script executed:
# Check remaining public pages for SEO imports
for page in "src/pages/cart/index.js" "src/pages/checkout/index.js" "src/pages/login/index.js" "src/pages/register/index.js" "src/pages/gallery/index.js" "src/pages/team/index.js" "src/pages/tickets/index.js" "src/pages/events/[id].js"; do
echo "=== $page ===";
if [ -f "$page" ]; then
rg "SEO|import.*SEO" "$page" | head -5;
else
echo "NOT FOUND";
fi;
doneRepository: NexEvent/Citronics_2k26
Length of output: 851
🏁 Script executed:
# Check error pages (401, 404, 500)
for page in "src/pages/401.js" "src/pages/404.js" "src/pages/500.js"; do
echo "=== $page ===";
if [ -f "$page" ]; then
rg "SEO|Head|meta" "$page" | head -5;
else
echo "NOT FOUND";
fi;
doneRepository: NexEvent/Citronics_2k26
Length of output: 141
🏁 Script executed:
# Check all page files for SEO usage comprehensively
echo "=== Pages WITHOUT SEO usage ===";
for page in src/pages/*.js src/pages/*/*.js; do
if [[ "$page" != *"/_"* ]] && [[ "$page" != *"/api/"* ]] && [ -f "$page" ]; then
if ! rg -q "SEO|import.*Head" "$page"; then
echo "$page";
fi;
fi;
doneRepository: NexEvent/Citronics_2k26
Length of output: 348
🏁 Script executed:
# Examine a page-specific SEO component to see what metadata it provides
head -40 src/components/SEO/pages/HomeSEO.jsRepository: NexEvent/Citronics_2k26
Length of output: 1575
🏁 Script executed:
# Check if about/index.js, login/index.js, register/index.js import their SEO components
head -20 src/pages/about/index.js
echo "---"
head -20 src/pages/login/index.js
echo "---"
head -20 src/pages/register/index.jsRepository: NexEvent/Citronics_2k26
Length of output: 2306
🏁 Script executed:
# Check what's in dashboard/index.js and checkout/payment-status.js
head -30 src/pages/dashboard/index.js
echo "---"
head -30 src/pages/checkout/payment-status.jsRepository: NexEvent/Citronics_2k26
Length of output: 1794
🏁 Script executed:
# List all SEO component files available
ls -la src/components/SEO/pages/Repository: NexEvent/Citronics_2k26
Length of output: 800
Wire up SEO components on all public routes; currently missing from About, Login, Register pages.
SEO components exist for most routes, but about/index.js, login/index.js, and register/index.js have corresponding *SEO.js components defined but are not importing them. Additionally, tickets/index.js, tickets/verify.js, and checkout/payment-status.js currently have no SEO coverage at all. Since the global <Head> in _app.js only includes viewport and PWA metadata (no title, description, or OG tags), these uncovered routes will render without proper SEO/share-preview metadata. Verify whether these remaining pages should have SEO coverage and either wire them up or update the comment to reflect what is intentionally excluded.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/_app.js` around lines 97 - 100, Pages about/index.js,
login/index.js, and register/index.js are missing their respective SEO
components and several routes (tickets/index.js, tickets/verify.js,
checkout/payment-status.js) have no SEO coverage while _app.js only provides
minimal global Head metadata; import and render each page's corresponding SEO
component (e.g., AboutSEO, LoginSEO, RegisterSEO) at the top of those page
components so title/description/OG tags are present, and for tickets/* and
checkout/payment-status either (A) add new page-specific SEO components (e.g.,
TicketsSEO, TicketVerifySEO, PaymentStatusSEO) and include them in the pages or
(B) if omission is intentional, update the comment near the global <Head> in
_app.js to explicitly list and justify excluded routes; ensure you reference the
page component names (about/index.js, login/index.js, register/index.js,
tickets/index.js, tickets/verify.js, checkout/payment-status.js) when making the
changes.
| const seoEvent = { | ||
| id: event.id ?? null, | ||
| name: event.name ?? null, | ||
| tagline: event.tagline ?? null, | ||
| description: event.description ?? null, | ||
| venue: event.venue ?? null, | ||
| ticket_price: event.ticket_price ?? null, | ||
| banner_url: event.banner_url ?? null, | ||
| start_time: event.start_time ? String(event.start_time) : null, | ||
| end_time: event.end_time ? String(event.end_time) : null, | ||
| department_name: event.department_name ?? null, | ||
| } |
There was a problem hiding this comment.
Map the SEO payload from the fields getEventById actually returns.
src/services/event-service.js:153-206 aliases the event name as title, the department as departmentName, and does not return banner_url. Lines 42-51 therefore serialize mostly null, so EventDetailSEO falls back to generic title/description and drops the per-event OG image.
Suggested fix
const seoEvent = {
id: event.id ?? null,
- name: event.name ?? null,
+ name: event.title ?? null,
tagline: event.tagline ?? null,
description: event.description ?? null,
venue: event.venue ?? null,
ticket_price: event.ticket_price ?? null,
- banner_url: event.banner_url ?? null,
+ banner_url: Array.isArray(event.images)
+ ? (event.images[0] ?? null)
+ : (event.images ?? null),
start_time: event.start_time ? String(event.start_time) : null,
end_time: event.end_time ? String(event.end_time) : null,
- department_name: event.department_name ?? null,
+ department_name: event.departmentName ?? 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.
| const seoEvent = { | |
| id: event.id ?? null, | |
| name: event.name ?? null, | |
| tagline: event.tagline ?? null, | |
| description: event.description ?? null, | |
| venue: event.venue ?? null, | |
| ticket_price: event.ticket_price ?? null, | |
| banner_url: event.banner_url ?? null, | |
| start_time: event.start_time ? String(event.start_time) : null, | |
| end_time: event.end_time ? String(event.end_time) : null, | |
| department_name: event.department_name ?? null, | |
| } | |
| const seoEvent = { | |
| id: event.id ?? null, | |
| name: event.title ?? null, | |
| tagline: event.tagline ?? null, | |
| description: event.description ?? null, | |
| venue: event.venue ?? null, | |
| ticket_price: event.ticket_price ?? null, | |
| banner_url: Array.isArray(event.images) | |
| ? (event.images[0] ?? null) | |
| : (event.images ?? null), | |
| start_time: event.start_time ? String(event.start_time) : null, | |
| end_time: event.end_time ? String(event.end_time) : null, | |
| department_name: event.departmentName ?? null, | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/events/`[id].js around lines 41 - 52, The seoEvent mapping uses
fields that getEventById doesn't return; update the properties to match the
service aliases so EventDetailSEO receives real values: use event.title ?? null
for the name/title, event.departmentName ?? null for department_name, and
event.bannerUrl ?? null for banner_url (keep the start/end time conversions
as-is but verify field names if getEventById uses startTime/endTime). Replace
the current seoEvent keys (e.g., name, department_name, banner_url) to pull from
event.title, event.departmentName, and event.bannerUrl so EventDetailSEO gets
the per-event title, department and OG image.
- robots.txt: remove cart/checkout disallow (handled by noindex meta), scope /_next/ rules to allow static assets but block image optimizer - Fix HTML entity encoding bug (") in HomeSEO, GallerySEO, RegisterSEO, TeamSEO descriptions — now uses proper apostrophe - SEOHead: escape </script> sequences in JSON-LD via \u003c to prevent XSS - seo.config: add centralized fest stats (competitionCount, participantCount, departmentCount) — schemas.js now interpolates instead of hardcoding - schemas.js: remove contradictory fest-level offers block, set isAccessibleForFree: true (fest entry is free, individual events vary) - events/[id].js: fix seoEvent field mapping — event.title (not .name), event.images (not .banner_url), event.departmentName (not .department_name) - Home page: render HomeSEO inside loading/error early returns so meta tags are always present regardless of data-fetch state - Add SEO to all remaining pages: about, login, register, tickets, tickets/verify, checkout/payment-status - Create TicketsSEO, TicketVerifySEO, PaymentStatusSEO components (noindex) - sitemap.xml: paginate event fetch loop instead of hard-capping at 500
| <script | ||
| key={`ld-${i}`} | ||
| type='application/ld+json' | ||
| dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\u003c') }} |
Check warning
Code scanning / CodeQL
Replacement of a substring with itself Medium
Copilot Autofix
AI 3 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (2)
src/components/SEO/SEOHead.js (1)
84-84:⚠️ Potential issue | 🔴 CriticalCritical: The JSON-LD escaping is ineffective — XSS vulnerability remains.
The replacement
.replace(/</g, '\u003c')does nothing because'\u003c'is a JavaScript Unicode escape that evaluates to the literal<character at parse time. This means you're replacing<with<, leaving the</script>injection vector open.To emit the literal string
\u003cin the JSON output (which browsers will interpret as<when parsing JSON-LD), you need to escape the backslash:'\\u003c'.Fix the escaping to actually prevent XSS
{schemas.map((schema, i) => ( <script key={`ld-${i}`} type='application/ld+json' - dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\u003c') }} + dangerouslySetInnerHTML={{ __html: JSON.stringify(schema).replace(/</g, '\\u003c') }} /> ))}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SEO/SEOHead.js` at line 84, The replace call in the SEOHead component is ineffective and leaves a XSS vector: update the expression that sets dangerouslySetInnerHTML for JSON.stringify(schema) to replace "<" with the literal backslash-u sequence by using a double-escaped backslash (i.e., replace(/</g, '\\u003c') ) so the output contains the string "\u003c" rather than the parsed "<"; locate the JSON.stringify(schema) usage in the SEOHead component and change the replace argument accordingly (or alternatively use a safe serializer such as serialize-javascript to produce safe JSON-LD).src/pages/events/[id].js (1)
41-52:⚠️ Potential issue | 🟠 MajorNormalize
event.imagesbefore assigningbanner_url.
eventSchema()expectsbanner_urlto be a single URL and wraps it in an array itself. Passingevent.imagesthrough unchanged here will re-break the event image metadata when the service returns multiple images, because the schema ends up with a nested/invalid image value.Proposed fix
const seoEvent = { id: event.id ?? null, name: event.title ?? null, tagline: event.tagline ?? null, description: event.description ?? null, venue: event.venue ?? null, ticket_price: event.ticket_price ?? null, - banner_url: event.images ?? null, + banner_url: Array.isArray(event.images) + ? (event.images[0] ?? null) + : (event.images ?? null), start_time: event.start_time ? String(event.start_time) : null, end_time: event.end_time ? String(event.end_time) : null, department_name: event.departmentName ?? null, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/events/`[id].js around lines 41 - 52, seoEvent builds banner_url from event.images which may be an array; normalize banner_url to a single URL (or null) before assigning so eventSchema receives a single string instead of an array. Update the seoEvent assignment for banner_url to extract the first image when event.images is an array (e.g., event.images?.[0]), handle the case where event.images is already a string, and coerce/validate to a string or null (use String(...) or null fallback) so banner_url passed to eventSchema is always a single URL string or null.
🧹 Nitpick comments (3)
src/components/SEO/schemas.js (1)
182-190: HardcodedvalidFromdate will become stale.The
validFrom: '2025-12-01'is hardcoded and will need manual updates for future fest editions. Consider deriving this fromSITEconfig or making it relative to the fest start date.♻️ Derive validFrom from config
availability: 'https://schema.org/InStock', - validFrom: '2025-12-01', + validFrom: SITE.ticketSaleStartDate || '2025-12-01', ...(isFree && { description: 'Free entry' }),Then add to
seo.config.js:ticketSaleStartDate: '2025-12-01',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SEO/schemas.js` around lines 182 - 190, The schema's offers.validFrom is hardcoded ('2025-12-01') which will become stale; update the offers construction in src/components/SEO/schemas.js to derive validFrom from configuration or the event data instead: read a ticket sale start date from the SITE/seo config (e.g., ticketSaleStartDate in seo.config.js) or fall back to the event start date (or current date) and format it as ISO YYYY-MM-DD before assigning to offers.validFrom; keep the existing conditional spread for isFree and ensure the symbol names to change are offers and validFrom in the same object.src/components/SEO/seo.config.js (1)
54-58:buildCanonicaldoesn't sanitize potentially malicious or malformed paths.The function only checks for a leading slash but doesn't validate or sanitize the path. Paths containing
.., query strings, or fragments could produce unexpected canonical URLs. Since this is consumed by per-page SEO components that pass hardcoded paths, the risk is low, but defensive validation would be safer.🛡️ Optional: Add path normalization
export function buildCanonical(path = '/') { - const clean = path.startsWith('/') ? path : `/${path}` - - return `${SITE.url}${clean}` + // Strip query/fragment, normalize slashes + let clean = path.split(/[?#]/)[0] + clean = clean.startsWith('/') ? clean : `/${clean}` + clean = clean.replace(/\/+/g, '/') + return `${SITE.url}${clean}` }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/SEO/seo.config.js` around lines 54 - 58, The buildCanonical function should normalize and sanitize the incoming path before concatenating with SITE.url: strip any query string and fragment, collapse duplicate slashes, resolve and remove any "." or ".." path segments, and ensure the result begins with a single leading slash; update buildCanonical to perform these checks and transformations (use a URL/path-normalization approach or the Node/whatwg URL APIs) so that SITE.url + cleanedPath always produces a safe canonical URL while still accepting default '/' when path is empty or invalid.src/pages/sitemap.xml.js (1)
67-70: Silent catch block may mask underlying issues.The empty catch block silently falls back to a static-only sitemap when the database is unreachable, which is reasonable for resilience. However, this could mask persistent database issues in production. Consider logging the error for observability.
📊 Add error logging for observability
- } catch { + } catch (err) { // If DB is unreachable, serve a static-only sitemap rather than failing + console.error('[sitemap.xml] Failed to fetch events:', err.message) events = [] }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/pages/sitemap.xml.js` around lines 67 - 70, The catch block that sets events = [] in the sitemap generator silently swallows errors; update the try/catch around the DB fetch (the block that assigns events) to log the caught error (including error.message or stack) via the existing logger or console.error before falling back to a static-only sitemap so production DB failures are observable; reference the variable events and the catch handling in the sitemap generation function to locate where to add the log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/SEO/pages/HomeSEO.js`:
- Around line 17-19: The HomeSEO.js metadata is hardcoded (e.g., "April 7–9,
2026") and must be built from the centralized seo.config.js instead: import the
shared fest config (e.g., the exported fields in seo.config.js such as fest
name, dates, eventsCount, participants, organizer/location) into
src/components/SEO/pages/HomeSEO.js and compose title, description and keywords
using those values (replace literal "April 7–9, 2026" and numeric literals with
the config properties) so the homepage metadata uses the single source of truth
and cannot drift from the values used elsewhere.
In `@src/pages/events/`[id].js:
- Around line 29-39: The getServerSideProps function currently treats malformed
IDs and missing events as props with seoEvent:null; change it to strictly
validate the route segment (e.g., ensure params.id matches /^\d+$/) and if the
ID is invalid or empty return { notFound: true } instead of props, then call
eventService.getEventById(parseInt(id)) and if that returns null also return {
notFound: true }; remove or avoid swallowing unexpected exceptions so they
bubble (do not return seoEvent:null for errors). Update the logic around
getServerSideProps, params, parseInt(id), and eventService.getEventById to
implement this behavior.
In `@src/pages/sitemap.xml.js`:
- Around line 81-90: The sitemap mapping currently builds event URLs using
event.id which can produce /events/undefined for malformed items; update the
logic that produces eventEntries (the code using events.map and buildUrlEntry)
to first filter out events that do not have a truthy valid id (e.g.,
events.filter(e => e && e.id)) before mapping, so only events with a defined id
are passed into buildUrlEntry (also consider trimming/validating the id if
needed).
---
Duplicate comments:
In `@src/components/SEO/SEOHead.js`:
- Line 84: The replace call in the SEOHead component is ineffective and leaves a
XSS vector: update the expression that sets dangerouslySetInnerHTML for
JSON.stringify(schema) to replace "<" with the literal backslash-u sequence by
using a double-escaped backslash (i.e., replace(/</g, '\\u003c') ) so the output
contains the string "\u003c" rather than the parsed "<"; locate the
JSON.stringify(schema) usage in the SEOHead component and change the replace
argument accordingly (or alternatively use a safe serializer such as
serialize-javascript to produce safe JSON-LD).
In `@src/pages/events/`[id].js:
- Around line 41-52: seoEvent builds banner_url from event.images which may be
an array; normalize banner_url to a single URL (or null) before assigning so
eventSchema receives a single string instead of an array. Update the seoEvent
assignment for banner_url to extract the first image when event.images is an
array (e.g., event.images?.[0]), handle the case where event.images is already a
string, and coerce/validate to a string or null (use String(...) or null
fallback) so banner_url passed to eventSchema is always a single URL string or
null.
---
Nitpick comments:
In `@src/components/SEO/schemas.js`:
- Around line 182-190: The schema's offers.validFrom is hardcoded ('2025-12-01')
which will become stale; update the offers construction in
src/components/SEO/schemas.js to derive validFrom from configuration or the
event data instead: read a ticket sale start date from the SITE/seo config
(e.g., ticketSaleStartDate in seo.config.js) or fall back to the event start
date (or current date) and format it as ISO YYYY-MM-DD before assigning to
offers.validFrom; keep the existing conditional spread for isFree and ensure the
symbol names to change are offers and validFrom in the same object.
In `@src/components/SEO/seo.config.js`:
- Around line 54-58: The buildCanonical function should normalize and sanitize
the incoming path before concatenating with SITE.url: strip any query string and
fragment, collapse duplicate slashes, resolve and remove any "." or ".." path
segments, and ensure the result begins with a single leading slash; update
buildCanonical to perform these checks and transformations (use a
URL/path-normalization approach or the Node/whatwg URL APIs) so that SITE.url +
cleanedPath always produces a safe canonical URL while still accepting default
'/' when path is empty or invalid.
In `@src/pages/sitemap.xml.js`:
- Around line 67-70: The catch block that sets events = [] in the sitemap
generator silently swallows errors; update the try/catch around the DB fetch
(the block that assigns events) to log the caught error (including error.message
or stack) via the existing logger or console.error before falling back to a
static-only sitemap so production DB failures are observable; reference the
variable events and the catch handling in the sitemap generation function to
locate where to add the log.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6ef4d077-4386-45ae-a9b9-891e3e1d2831
📒 Files selected for processing (21)
public/robots.txtsrc/components/SEO/SEOHead.jssrc/components/SEO/index.jssrc/components/SEO/pages/GallerySEO.jssrc/components/SEO/pages/HomeSEO.jssrc/components/SEO/pages/PaymentStatusSEO.jssrc/components/SEO/pages/RegisterSEO.jssrc/components/SEO/pages/TeamSEO.jssrc/components/SEO/pages/TicketVerifySEO.jssrc/components/SEO/pages/TicketsSEO.jssrc/components/SEO/schemas.jssrc/components/SEO/seo.config.jssrc/pages/about/index.jssrc/pages/checkout/payment-status.jssrc/pages/events/[id].jssrc/pages/index.jssrc/pages/login/index.jssrc/pages/register/index.jssrc/pages/sitemap.xml.jssrc/pages/tickets/index.jssrc/pages/tickets/verify.js
🚧 Files skipped from review as they are similar to previous changes (6)
- src/components/SEO/pages/TeamSEO.js
- public/robots.txt
- src/components/SEO/index.js
- src/components/SEO/pages/GallerySEO.js
- src/pages/index.js
- src/components/SEO/pages/RegisterSEO.js
| title='Citronics 2026 — CDGI Annual Tech Fest | April 7–9 | 35+ Events, 5000+ Participants' | ||
| description="Citronics 2026 is Central India's largest annual college tech fest hosted by Chameli Devi Group of Institutions (CDGI), Indore. Featuring 35+ competitions in AI, robotics, coding, management & cultural events — April 7 to 9, 2026. Register now and compete!" | ||
| keywords='Citronics 2026, Citronics 2k26, CDGI Citronics, CDGI tech fest 2026, Chameli Devi Group of Institutions, CDGI Indore, Central India tech fest, Indore technical festival, Madhya Pradesh engineering fest, college tech fest 2026, national level technical fest, engineering fest India, annual tech fest, register Citronics 2026, event registration college, buy event tickets online, college fest tickets, annual fest CDGI April 2026, technical cultural fest, robotics competition, coding competition, hackathon Indore' |
There was a problem hiding this comment.
Use the shared fest config here instead of hardcoded dates/stats.
The homepage metadata now hardcodes April 7–9, 2026, but src/pages/about/index.js Line 412 still says March 2026. That gives crawlers and users conflicting event details. Since this PR already centralizes fest stats in seo.config.js, build the title/description/keywords from that shared source so these values cannot drift.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/SEO/pages/HomeSEO.js` around lines 17 - 19, The HomeSEO.js
metadata is hardcoded (e.g., "April 7–9, 2026") and must be built from the
centralized seo.config.js instead: import the shared fest config (e.g., the
exported fields in seo.config.js such as fest name, dates, eventsCount,
participants, organizer/location) into src/components/SEO/pages/HomeSEO.js and
compose title, description and keywords using those values (replace literal
"April 7–9, 2026" and numeric literals with the config properties) so the
homepage metadata uses the single source of truth and cannot drift from the
values used elsewhere.
| export async function getServerSideProps({ params }) { | ||
| try { | ||
| const { id } = params | ||
| if (!id || isNaN(parseInt(id))) return { props: { seoEvent: null } } | ||
|
|
||
| // Import server-side service directly — avoids an extra HTTP round-trip | ||
| const eventService = (await import('src/services/event-service')).default | ||
| const event = await eventService.getEventById(parseInt(id)) | ||
|
|
||
| // Serialize only the fields needed for SEO (keeps payload small) | ||
| if (!event) return { props: { seoEvent: null } } |
There was a problem hiding this comment.
Return a real 404 for malformed or missing event IDs.
parseInt(id) will accept values like 123abc, and both the invalid-ID and missing-event paths fall back to { props: { seoEvent: null } }, so bad URLs can still return HTTP 200 with generic content. That creates duplicate URLs and soft-404s. Validate the whole segment strictly, return { notFound: true } when the ID is invalid or the event is absent, and let unexpected exceptions bubble so Next can render a 500.
Proposed fix
export async function getServerSideProps({ params }) {
try {
- const { id } = params
- if (!id || isNaN(parseInt(id))) return { props: { seoEvent: null } }
+ const { id } = params ?? {}
+ if (typeof id !== 'string' || !/^\d+$/.test(id)) {
+ return { notFound: true }
+ }
// Import server-side service directly — avoids an extra HTTP round-trip
const eventService = (await import('src/services/event-service')).default
- const event = await eventService.getEventById(parseInt(id))
+ const event = await eventService.getEventById(Number(id))
// Serialize only the fields needed for SEO (keeps payload small)
- if (!event) return { props: { seoEvent: null } }
+ if (!event) return { notFound: true }
@@
- } catch {
- return { props: { seoEvent: null } }
+ } catch (error) {
+ throw error
}
}Also applies to: 55-56
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/events/`[id].js around lines 29 - 39, The getServerSideProps
function currently treats malformed IDs and missing events as props with
seoEvent:null; change it to strictly validate the route segment (e.g., ensure
params.id matches /^\d+$/) and if the ID is invalid or empty return { notFound:
true } instead of props, then call eventService.getEventById(parseInt(id)) and
if that returns null also return { notFound: true }; remove or avoid swallowing
unexpected exceptions so they bubble (do not return seoEvent:null for errors).
Update the logic around getServerSideProps, params, parseInt(id), and
eventService.getEventById to implement this behavior.
| const eventEntries = (events || []).map(event => | ||
| buildUrlEntry({ | ||
| loc: `${SITE_URL}/events/${event.id}`, | ||
| lastmod: event.updated_at | ||
| ? new Date(event.updated_at).toISOString().slice(0, 10) | ||
| : today, | ||
| changefreq: 'weekly', | ||
| priority: '0.8', | ||
| }) | ||
| ) |
There was a problem hiding this comment.
Potential issue if event.id is undefined.
If an event object lacks an id field (malformed data), the sitemap would contain /events/undefined. Consider filtering out events without valid IDs.
🛡️ Filter events with valid IDs
- const eventEntries = (events || []).map(event =>
+ const eventEntries = events.filter(e => e?.id).map(event =>
buildUrlEntry({
loc: `${SITE_URL}/events/${event.id}`,📝 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 eventEntries = (events || []).map(event => | |
| buildUrlEntry({ | |
| loc: `${SITE_URL}/events/${event.id}`, | |
| lastmod: event.updated_at | |
| ? new Date(event.updated_at).toISOString().slice(0, 10) | |
| : today, | |
| changefreq: 'weekly', | |
| priority: '0.8', | |
| }) | |
| ) | |
| const eventEntries = (events || []).filter(e => e?.id).map(event => | |
| buildUrlEntry({ | |
| loc: `${SITE_URL}/events/${event.id}`, | |
| lastmod: event.updated_at | |
| ? new Date(event.updated_at).toISOString().slice(0, 10) | |
| : today, | |
| changefreq: 'weekly', | |
| priority: '0.8', | |
| }) | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/pages/sitemap.xml.js` around lines 81 - 90, The sitemap mapping currently
builds event URLs using event.id which can produce /events/undefined for
malformed items; update the logic that produces eventEntries (the code using
events.map and buildUrlEntry) to first filter out events that do not have a
truthy valid id (e.g., events.filter(e => e && e.id)) before mapping, so only
events with a defined id are passed into buildUrlEntry (also consider
trimming/validating the id if needed).
Description
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context.
Fixes # (issue)
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
Checklist:
Summary by CodeRabbit
New Features
Enhancements