fix(seo-inject): add JSON-LD structured data injection#3670
fix(seo-inject): add JSON-LD structured data injection#3670PierreBrisorgueil merged 2 commits intomasterfrom
Conversation
Generate a JSON-LD script tag from seo.schema config when enabled. Supports Person/Organization types with optional jobTitle, sameAs, and image fields. Values are not HTML-escaped since they live inside a JSON string in a script tag. Closes #3664
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe seo-inject plugin now generates and injects a Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3670 +/- ##
==========================================
+ Coverage 91.03% 91.16% +0.12%
==========================================
Files 21 21
Lines 636 645 +9
Branches 170 176 +6
==========================================
+ Hits 579 588 +9
Misses 57 57 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds build-time JSON-LD structured data injection to the seo-inject Vite plugin so configured app.seo.schema values can be emitted into the built index.html head (addressing missing functionality described in #3664).
Changes:
- Inject
<script type="application/ld+json">JSON-LD block whenseo.schema.enabledandseo.schema.typeare set. - Add unit tests covering JSON-LD injection, omission rules, and optional fields.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/lib/plugins/seo-inject.js | Adds JSON-LD structured data generation/injection into the transformed HTML head. |
| src/lib/plugins/tests/seo-inject.spec.js | Adds unit tests validating JSON-LD injection behavior and optional fields. |
Address Copilot review: replace < with \u003c in the serialized JSON-LD string to prevent config values containing </script> from breaking out of the script tag. Add regression test for this XSS vector.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/lib/plugins/seo-inject.js (1)
119-133: Add JSDoc for the modifiedtransformIndexHtmlhook.This change updates the hook, but it still has no JSDoc for
htmland the returned HTML string.📝 Suggested fix
return { name: 'seo-inject', + /** + * Inject SEO tags and structured data into the HTML document. + * + * `@param` {string} html - Source `index.html` contents. + * `@returns` {string} HTML with injected SEO markup. + */ transformIndexHtml(html) { const tags = [];As per coding guidelines, "Every new or modified function must have a JSDoc header with one-line description,
@paramfor each argument, and@returnsfor any non-void return value (always include@returnsfor async functions)".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/plugins/seo-inject.js` around lines 119 - 133, Add a JSDoc header to the modified transformIndexHtml hook in seo-inject.js: include a one-line description, an `@param` for the html argument (string) and any other parameters used (e.g., ctx or opts) with types and brief descriptions, and an `@returns` describing that the function returns a (possibly Promise-wrapped) HTML string; ensure the JSDoc notes whether the function is async and documents the returned string.src/lib/plugins/tests/seo-inject.spec.js (1)
304-326: Add a regression test for theschema.namefallback.One of the core requirements from
#3664is thatnamefalls back toapp.titlewhenschema.nameis absent, but this block only exercises the explicitschema.namepath.✅ Suggested test
describe('JSON-LD structured data', () => { it('injects JSON-LD when schema.enabled is true', () => { const config = { app: { title: 'Test App', description: 'Test description', url: 'https://example.com', seo: { schema: { enabled: true, type: 'Person', name: 'Test User' }, }, }, }; const result = transform(config); expect(result).toContain('<script type="application/ld+json">'); const match = result.match(/<script type="application\/ld\+json">(.*?)<\/script>/); expect(match).not.toBeNull(); const jsonLd = JSON.parse(match[1]); expect(jsonLd['@context']).toBe('https://schema.org'); expect(jsonLd['@type']).toBe('Person'); expect(jsonLd.name).toBe('Test User'); expect(jsonLd.url).toBe('https://example.com'); expect(jsonLd.description).toBe('Test description'); }); + + it('falls back to app.title when schema.name is missing', () => { + const config = { + app: { + title: 'Test App', + description: 'Test description', + url: 'https://example.com', + seo: { + schema: { enabled: true, type: 'Person' }, + }, + }, + }; + const result = transform(config); + const match = result.match(/<script type="application\/ld\+json">(.*?)<\/script>/); + const jsonLd = JSON.parse(match[1]); + expect(jsonLd.name).toBe('Test App'); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/plugins/tests/seo-inject.spec.js` around lines 304 - 326, Add a regression test that verifies schema.name falls back to app.title when schema.name is missing: create a new it-block (next to the existing JSON-LD test) that builds a config with app.title/app.description/app.url and seo.schema: { enabled: true, type: 'Person' } but without schema.name, call transform(config), extract the JSON-LD script the same way as the existing test, JSON.parse it and assert jsonLd.name === app.title (and keep assertions for `@context`, `@type`, url and description to ensure all other fields remain correct); use the same helper/regex and the transform function reference so the test mirrors the original but verifies the fallback behavior.
🤖 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/lib/plugins/seo-inject.js`:
- Around line 122-132: The JSON-LD payload built in jsonLd and injected via
tags.push(` <script
type="application/ld+json">${JSON.stringify(jsonLd)}</script>`) can be
terminated by unescaped values (e.g., schema.name or app.description); change to
a script-safe serialization: produce the JSON string from jsonLd (using
JSON.stringify) then escape any occurrences that would break a script tag (for
example replace the sequence "</script" with "<\/script>" and optionally escape
"<!--" / "-->"), and use that escaped string in the tags.push call (or factor
into a helper like safeJsonStringify/jsonLdToScript to centralize the escaping).
---
Nitpick comments:
In `@src/lib/plugins/seo-inject.js`:
- Around line 119-133: Add a JSDoc header to the modified transformIndexHtml
hook in seo-inject.js: include a one-line description, an `@param` for the html
argument (string) and any other parameters used (e.g., ctx or opts) with types
and brief descriptions, and an `@returns` describing that the function returns a
(possibly Promise-wrapped) HTML string; ensure the JSDoc notes whether the
function is async and documents the returned string.
In `@src/lib/plugins/tests/seo-inject.spec.js`:
- Around line 304-326: Add a regression test that verifies schema.name falls
back to app.title when schema.name is missing: create a new it-block (next to
the existing JSON-LD test) that builds a config with
app.title/app.description/app.url and seo.schema: { enabled: true, type:
'Person' } but without schema.name, call transform(config), extract the JSON-LD
script the same way as the existing test, JSON.parse it and assert jsonLd.name
=== app.title (and keep assertions for `@context`, `@type`, url and description to
ensure all other fields remain correct); use the same helper/regex and the
transform function reference so the test mirrors the original but verifies the
fallback behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3586985d-9e79-4d6e-869d-5cc28af12735
📒 Files selected for processing (2)
src/lib/plugins/seo-inject.jssrc/lib/plugins/tests/seo-inject.spec.js
# [1.5.0](v1.4.0...v1.5.0) (2026-04-01) ### Bug Fixes * **analytics:** PostHog api_host bug + identify/group on login ([#3772](#3772)) ([534fdf3](534fdf3)), closes [#3753](#3753) [#3766](#3766) [#3769](#3769) [#3771](#3771) * **auth,admin:** move mailer warning to admin page, fix verify-email routing, align invite button ([0d195c4](0d195c4)) * **auth:** add missing $route.query mock in verifyEmail tests ([b2732f6](b2732f6)) * **auth:** address CodeRabbit review feedback ([1d14b06](1d14b06)) * **auth:** address review feedback from pass 1 ([bd27d2f](bd27d2f)) * **auth:** address review feedback from pass 1 ([a87fdb1](a87fdb1)) * **auth:** address review feedback from pass 2 — prevent form flash on load ([a6afeca](a6afeca)) * **auth:** fix runtime deprecation warnings and add auth view unit tests ([1901dad](1901dad)), closes [#3592](#3592) * **auth:** persist snackbar dismissal on implicit close ([5e464c3](5e464c3)) * **auth:** resolve lint errors and update test mocks for fetchServerConfig ([a642001](a642001)) * **billing:** add store unit tests and fix view import paths ([e370272](e370272)) * **billing:** address CodeRabbit review — guards, tests, JSDoc, price fallback ([f0f6657](f0f6657)) * **billing:** address CodeRabbit review feedback ([b195d2d](b195d2d)) * **billing:** address CodeRabbit review feedback ([8435d1c](8435d1c)) * **billing:** address CodeRabbit review feedback ([4187771](4187771)) * **billing:** address review — tier comparison, graceful failure, tests, naming ([7b00b91](7b00b91)) * **billing:** address review feedback — pricing card, fetch guard, router tests ([34cbda9](34cbda9)) * **billing:** address UI review findings ([b281a8a](b281a8a)) * **billing:** address UI review findings ([ead3eb0](ead3eb0)) * **billing:** address UI review findings ([b2d3897](b2d3897)) * **billing:** address UI review findings ([a756cd9](a756cd9)) * **billing:** align CASL guard subject with backend policy ([7e8287b](7e8287b)) * **billing:** align CASL guard subject with backend policy ([8e9b768](8e9b768)) * **billing:** correct plan field name, add enterprise badge, store cleanup ([#3733](#3733)) ([9b6e30d](9b6e30d)), closes [#3729](#3729) [#3730](#3730) [#3731](#3731) [#3732](#3732) [#3729](#3729) [#3730](#3730) [#3731](#3731) [#3732](#3732) * **billing:** correct test URL assertions to match store checkout URLs ([cd64628](cd64628)) * **billing:** correct view import paths to match lowercase filenames ([e2646dd](e2646dd)) * **billing:** default free plan display + E2E security tests ([#3751](#3751)) ([025534a](025534a)) * **billing:** fix branch coverage and E2E auth guard test ([2864cf7](2864cf7)) * **billing:** fix branch coverage and harden E2E auth test ([7b91edb](7b91edb)) * **billing:** fix error-handling tests and restore CASL guard on /billing route ([bcb15a6](bcb15a6)) * **billing:** fix plan merge — match by planId/name and build price objects ([1c1c765](1c1c765)) * **billing:** gate fetchSubscription behind org check and add missing [@returns](https://github.com/returns) JSDoc ([9dd15a7](9dd15a7)) * **billing:** harden URL validation, test assertions, and error handling ([44d9cc2](44d9cc2)) * **billing:** include canceled query param in checkout cancel URL ([9e28636](9e28636)) * **billing:** propagate errors, add CASL route metadata, rename views ([39343ba](39343ba)) * **billing:** remove duplicate import in E2E test ([02576f5](02576f5)) * **billing:** rename views, fix test assertions, address review feedback ([cb72130](cb72130)) * **billing:** separate checkout error from fetch error in pricing view ([78db8b1](78db8b1)) * **billing:** update store cancel URL to include canceled query param ([ca5f1a3](ca5f1a3)) * **billing:** validate portal URL and catch fetchSubscription rejection ([7534b6c](7534b6c)) * **ci:** ensure coverage thresholds pass with all modules ([8e0c970](8e0c970)), closes [#3710](#3710) * **ci:** remove ARC-specific conditionals from CI.yml ([#3806](#3806)) ([48747f3](48747f3)), closes [#3805](#3805) * **config:** add missing api section to config.test.js ([#3679](#3679)) ([fff9ffc](fff9ffc)), closes [#3676](#3676) * **config:** address review — use lodash-es, drop glob dep, add env warning ([1ec84fa](1ec84fa)) * **config:** address review feedback from pass 1 ([3336f09](3336f09)) * **config:** align app_title default between Dockerfile and hooks/build ([b08dbf3](b08dbf3)) * **config:** backward-compat fallback for sucessColor and improve warning ([1965557](1965557)) * **config:** fail fast when config is missing in production builds ([#3671](#3671)) ([c660ba4](c660ba4)), closes [#3669](#3669) * **config:** generateConfig ignores module env-specific configs ([#3827](#3827)) ([1ffd80e](1ffd80e)), closes [#3826](#3826) * **config:** remove [@desc](https://github.com/desc) tag from deepMerge JSDoc ([7c4b613](7c4b613)) * **config:** remove duplicate vuetify.theme block from app config merge ([5a583a7](5a583a7)) * **config:** remove unused imports and fix typo in generateConfig ([7b0c006](7b0c006)) * **config:** replace _.merge with deepMerge to fix array handling ([4e91025](4e91025)), closes [#3628](#3628) * **config:** skip undefined values and guard against prototype pollution in deepMerge ([ffc574e](ffc574e)) * **config:** use pathToFileURL for cross-platform dynamic imports ([acad44e](acad44e)) * **core:** address review feedback from pass 1 ([0f6e3af](0f6e3af)) * **core:** make datatable generic via fetchAction prop ([e3a4926](e3a4926)), closes [#3596](#3596) * **coverage:** exclude bootstrapper files with 0% coverage from collection ([4dfde5a](4dfde5a)) * **datatable:** increase per page select width to 100px ([#3779](#3779)) ([ff65d6f](ff65d6f)), closes [#3778](#3778) * **docker:** add DEVKIT_NODE_api_port to docker-compose.test.yml ([#3815](#3815)) ([0f06402](0f06402)), closes [#3809](#3809) * **docker:** make docker-compose.test.yml work for downstream projects ([#3790](#3790)) ([838aec1](838aec1)), closes [#3789](#3789) * **docs:** add src/ prefix and use <env> placeholder in merge order table ([5af5d5b](5af5d5b)) * **docs:** mark global env override config files as optional ([7459d0f](7459d0f)) * **e2e:** read ports from project config instead of hardcoding ([#3777](#3777)) ([ec56d20](ec56d20)), closes [#3775](#3775) * **header:** address review feedback from pass 1 ([b05d210](b05d210)) * **home,config:** address review feedback from pass 1 ([709ccbe](709ccbe)) * **home:** add JSDoc for tabs colorMode validator and tabStyle method ([a7fb5e3](a7fb5e3)) * **home:** add JSDoc to created() hook in home.team.view ([7548c15](7548c15)) * **home:** address CodeRabbit review feedback ([9990286](9990286)) * **home:** hero vertical centering, overlap prop, and Vuetify 4 typography migration ([4c26b25](4c26b25)), closes [#3587](#3587) * **home:** pass colorMode to tabs component so forced text color applies to tab items ([50256d8](50256d8)) * **lodash:** use lodash-es imports for proper tree-shaking ([50570cf](50570cf)) * **nav:** fix organizations ghost item icon in sidenav ([bca4237](bca4237)), closes [#3706](#3706) * **pull-request skill:** add consecutive_zero guard and re-check pending review checks ([18c70f8](18c70f8)) * **pull-request skill:** address review feedback — clarity and consistency ([2dd1ce2](2dd1ce2)) * **pull-request skill:** reply and resolve all threads including non-actionable ([ff776c8](ff776c8)) * **security:** harden auth views against XSS and weak validation ([#3735](#3735)) ([c3b844d](c3b844d)) * **seo-inject:** add JSON-LD structured data injection ([#3670](#3670)) ([5b36e5c](5b36e5c)), closes [#3664](#3664) * **seo:** improve heading hierarchy and alt text for accessibility ([#3661](#3661)) ([918f9bd](918f9bd)) * **seo:** puppeteer prerender Docker support + minor plugin fixes ([#3673](#3673)) ([8ac241b](8ac241b)) * **seo:** skip runtime JSON-LD when seo-inject handles schema ([#3677](#3677)) ([#3680](#3680)) ([b0d90ac](b0d90ac)) * **skill:** add merge conflict check in pull-request convergence loop ([ef136ed](ef136ed)), closes [#3707](#3707) * **skill:** address review feedback from pass 1 ([4020b82](4020b82)) * **skill:** handle CHANGES_REQUESTED, UNKNOWN mergeable, and standardize $PR usage ([13cb3cf](13cb3cf)) * **skill:** pull-request skill should ignore stack-level CodeRabbit comments for downstream projects ([97d2cb2](97d2cb2)), closes [#3604](#3604) * **skills:** clarify feature Phase 0 completeness check ([#3773](#3773)) ([8ac6b1b](8ac6b1b)) * **skills:** match coderabbitai[bot] login in monitoring jq filter ([aae850f](aae850f)) * **update-stack:** add concrete gh issue create command and resolution guidance ([6092368](6092368)) * **update-stack:** add downstream-only new files rule to conflict table ([4a15e39](4a15e39)) * **update-stack:** address review feedback from pass 1 ([f7ed820](f7ed820)) * **update-stack:** address review feedback from pass 2 ([2bef8a3](2bef8a3)) * **update-stack:** clarify failure origin criteria in step 3bis ([a71515e](a71515e)) ### Features * add /frontend design skill + migrate Vuetify 3 patterns to V4 ([#3644](#3644)) ([2f2a8ff](2f2a8ff)) * **analytics:** add useFeatureFlag and usePostHog composables ([#3774](#3774)) ([2506a4f](2506a4f)), closes [#3771](#3771) * **auth:** add email verification gate UI for org setup ([999aa60](999aa60)) * **auth:** add password visibility toggle ([#3752](#3752)) ([ea3b0a0](ea3b0a0)), closes [#336](#336) * **auth:** display server-side auth status on signin/signup pages ([0f4276d](0f4276d)) * **billing:** add billing page with plan badge and subscription management ([e9a1fc5](e9a1fc5)), closes [#3715](#3715) * **billing:** add BillingUsageBar component and enhance UpgradePrompt ([#3744](#3744)) ([0e9ca7f](0e9ca7f)) * **billing:** add checkout flow with auth/org guards and Stripe redirect ([ce17d67](ce17d67)), closes [#3714](#3714) * **billing:** add checkout flow with auth/org guards and Stripe redirect ([f762d91](f762d91)), closes [#3714](#3714) * **billing:** add feature gates — composable, upgrade prompt, router guard ([207c022](207c022)), closes [#3716](#3716) * **billing:** add homepage pricing section ([a8ced6d](a8ced6d)), closes [#3717](#3717) * **billing:** add pricing page with plan cards and billing toggle ([abb1d4a](abb1d4a)), closes [#3713](#3713) * **billing:** add pricing page with plan cards and billing toggle ([0881a9f](0881a9f)), closes [#3713](#3713) * **billing:** add useQuota composable for plan-based feature gating ([#3743](#3743)) ([85e2984](85e2984)) * **billing:** move pricing to dedicated page, update topnav ([eaab6f3](eaab6f3)) * **billing:** scaffold billing module with store, router, and views ([7b08b12](7b08b12)), closes [#3712](#3712) * **ci:** skip setup-node and playwright install on self-hosted runner ([#3803](#3803)) ([104d4f8](104d4f8)), closes [#3802](#3802) * **ci:** support configurable runner via RUNNER variable ([#3794](#3794)) ([1151d02](1151d02)), closes [#3792](#3792) * **ci:** use APP_ENV variable for generateConfig in CI ([#3801](#3801)) ([e384c7c](e384c7c)), closes [#3795](#3795) * **config:** migrate WAOS_VUE_* env prefix to DEVKIT_VUE_* ([7ad1a7f](7ad1a7f)), closes [#3614](#3614) * **header:** add float scroll behavior and document all config options ([d1d4891](d1d4891)) * **home:** add dedicated 404 Not Found page ([#3655](#3655)) ([ac73739](ac73739)) * **home:** add FAQ accordion component with JSON-LD schema ([#3821](#3821)) ([444e62c](444e62c)), closes [#3819](#3819) * **nav:** liquid glass sidenav with Apple-style inset mode ([#3737](#3737)) ([8203f2c](8203f2c)) * **pageHeader:** add tabs slot as alternative to icon + title ([#3781](#3781)) ([763d814](763d814)), closes [#3780](#3780) * phase 2 organizations ([#3702](#3702)) ([71c6a5c](71c6a5c)), closes [#3674](#3674) [#3675](#3675) [#3684](#3684) [#3686](#3686) [#3681](#3681) [#3682](#3682) [#3683](#3683) [#3684](#3684) [#3685](#3685) [#3686](#3686) [#3675](#3675) [#3674](#3674) * rename /frontend to /ui, add workflow rules ([#3704](#3704)) ([3d8259e](3d8259e)) * **sentry:** add Sentry error tracking and ErrorBoundary component ([#3788](#3788)) ([b5463d7](b5463d7)), closes [#3787](#3787) * **seo-inject:** add noscript fallback, preconnect hints, and theme-color ([#3663](#3663)) ([261a3a8](261a3a8)) * **seo:** add pre-rendering of home page at build time ([#3660](#3660)) ([39d9414](39d9414)) * **seo:** add seo-static Vite plugin for robots.txt, sitemap.xml, manifest.json ([#3658](#3658)) ([0a50388](0a50388)) * **update-stack:** report upstream issues when verify fails on stack code ([71dbecc](71dbecc)) * **verify:** add coverage enforcement to verify skill ([#3776](#3776)) ([63301f9](63301f9)) ### Performance Improvements * **ci:** optimize GitHub Actions ([#3793](#3793)) ([e31287e](e31287e)), closes [#3791](#3791)
Summary
seo-injectVite pluginseo.schemawithenabled,type,name,jobTitle,sameAs, etc., but the plugin had no code to read these values or emit a<script type="application/ld+json">blockScope
src/lib/plugins/seo-inject.jsnonelowValidation
npm run lintnpm run test:unitnpm run buildGuardrails check
.env*,secrets/**, keys, tokens)Notes for reviewers
<script>tag, not in HTML attributes. The values come from build-time config (not user input).Summary by CodeRabbit
Release Notes
New Features
Tests