feat: add structured data (FAQPage & SoftwareApplication) for improved SEO#142
feat: add structured data (FAQPage & SoftwareApplication) for improved SEO#142charuljain02 wants to merge 2 commits intoAOSSIE-Org:mainfrom
Conversation
📝 WalkthroughWalkthroughSEO enhancements and structured data additions to the frontend application. The root layout metadata expanded with keywords, Open Graph, and Twitter tags, plus JSON-LD SoftwareApplication data. FAQ section with structured data and FAQPage JSON-LD added to the home page. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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.
Actionable comments posted: 2
🧹 Nitpick comments (5)
frontend/app/layout.tsx (2)
56-70:dangerouslySetInnerHTMLwithJSON.stringify— safe here, but note the sanitization recommendation.The static analysis XSS warning is a false positive for this hardcoded payload. However, Next.js documentation notes that
JSON.stringifydoes not sanitize malicious strings for XSS injection, and recommends replacing<with its Unicode equivalent\u003c, or using a community alternative such asserialize-javascript.Since the FAQ and SoftwareApplication payloads are fully static today this is not a current risk, but if the data ever becomes dynamic (e.g., pulled from a CMS), applying the sanitization upfront will prevent a future vulnerability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/layout.tsx` around lines 56 - 70, The script uses dangerouslySetInnerHTML with JSON.stringify (the JSON-LD payload in layout.tsx) which is currently static but triggers an XSS concern; fix by sanitizing the stringified JSON before injecting—either use a safe serializer like serialize-javascript to produce the JSON-LD or post-process the JSON.stringify output to replace every "<" with its Unicode escape (e.g., replace(/</g, '\\u003c')) so the value passed to dangerouslySetInnerHTML (and produced by JSON.stringify in the same block) cannot introduce script tags if the payload becomes dynamic.
24-24: Hardcoded deployment URL is repeated acrosslayout.tsxandpage.tsx.
"https://perspective-aossie.vercel.app/"appears in three places:openGraph.url(line 24), the JSON-LDurlfield (line 67), andpage.tsxline 386. A typo or domain migration will silently break structured data and OG tags. Extract it to a shared constant or read fromprocess.env.NEXT_PUBLIC_SITE_URL.♻️ Extract to a constant
+// e.g. lib/constants.ts +export const SITE_URL = + process.env.NEXT_PUBLIC_SITE_URL ?? "https://perspective-aossie.vercel.app/";Then reference
SITE_URLinlayout.tsxandpage.tsx.Also applies to: 56-70
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/layout.tsx` at line 24, The hardcoded site URL is duplicated in openGraph.url (layout.tsx), the JSON-LD url field, and page.tsx; extract it into a single source (e.g., const SITE_URL) or read from process.env.NEXT_PUBLIC_SITE_URL and replace all hardcoded occurrences. Update the openGraph object (openGraph.url), the JSON-LD construction (the url field around line 67 in layout.tsx), and the usage in page.tsx so they all reference SITE_URL (or the env-backed constant) instead of the literal "https://perspective-aossie.vercel.app/". Ensure the constant is exported/imported where needed so both layout.tsx and page.tsx use the same value.frontend/app/page.tsx (3)
354-372: JSON-LD injected vianext/script(afterInteractive) is absent from the initial SSR HTML.
afterInteractivescripts are injected into the HTML client-side and will load after some (or all) hydration occurs on the page — they are not present in the server-rendered HTML response. Next.js's current recommendation for JSON-LD is to render structured data as a<script>tag inlayout.jsorpage.jscomponents (a plain<script>tag, notnext/script).Because
page.tsxis"use client", a raw<script>tag can't be rendered here without risking hydration mismatches. The cleanest fix is to move theFAQPageJSON-LD intolayout.tsx(a server component) alongside the existingSoftwareApplicationblock, sincefaqDatais fully static:♻️ Proposed fix – move FAQPage JSON-LD to layout.tsx
In
layout.tsx:+import { faqData } from "@/data/faq"; // or inline the array // inside RootLayout body, after the SoftwareApplication script: +<script + type="application/ld+json" + dangerouslySetInnerHTML={{ + __html: JSON.stringify({ + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: faqData.map((faq) => ({ + "@type": "Question", + name: faq.question, + acceptedAnswer: { "@type": "Answer", text: faq.answer }, + })), + }), + }} +/>In
page.tsx, remove:- {/* FAQ Structured Data */} - <Script - id="faq-schema" - type="application/ld+json" - dangerouslySetInnerHTML={{ ... }} - />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/page.tsx` around lines 354 - 372, The FAQ JSON-LD is being rendered via next/script in the client component (page.tsx with "use client") so it won't appear in SSR HTML; move the FAQ structured-data block that builds the FAQPage from faqData into the server component layout (layout.tsx) alongside the existing SoftwareApplication JSON-LD, remove the next/Script usage from page.tsx, and render a plain <script type="application/ld+json"> with JSON.stringify(...) of the same object in layout.tsx so the FAQ JSON-LD is present in the initial server response; keep faqData as-is since it is static.
95-116:faqDatais suitable as a module-level constant or shared data file.Since
faqDatais entirely static and drives both the rendered FAQ UI and the JSON-LD schema, keeping it inside the component body means it's re-created on every render and can't be co-located with the structured data in a server component. Moving it outside (or to a shared data file) also makes it easier to deduplicate with layout-level JSON-LD injection.♻️ Suggested move
"use client"; import { useRouter } from "next/navigation"; + +const faqData = [ + { + question: "What is Perspective?", + answer: + "Perspective is an AI-powered research tool that analyzes online articles to uncover bias and generate balanced counter-perspectives using advanced NLP models.", + }, + // ... remaining entries +]; + import { Button } from "@/components/ui/button"; import Script from "next/script"; // ... export default function Home() { const router = useRouter(); - const faqData = [ ... ];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/page.tsx` around lines 95 - 116, The faqData array is declared inside the component, causing it to be re-created on every render and preventing reuse in server-level JSON-LD; move the faqData variable to module scope (or a shared data file) so it becomes a single exported constant that can be imported by the page component and by any layout/server component that injects the JSON-LD; update references in page.tsx to use the module-level faqData (or imported constant) and ensure any JSON-LD generation uses the same exported constant to avoid duplication.
301-302: Indentation inconsistency in the FAQ section comment.Line 301 (
{/* FAQ Section */}) has extra leading whitespace compared to the surrounding<section>tag at line 302.🔧 Fix
- {/* FAQ Section */} - <section className="container mx-auto px-4 py-12 md:py-20"> + {/* FAQ Section */} + <section className="container mx-auto px-4 py-12 md:py-20">Same applies to the
{/* FAQ Structured Data */}comment at line 354.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/page.tsx` around lines 301 - 302, Adjust the indentation of the inline JSX comments so they match the surrounding JSX; specifically, align the `{/* FAQ Section */}` and `{/* FAQ Structured Data */}` comments with the `<section className="container mx-auto px-4 py-12 md:py-20">` element (and other sibling tags) by removing the extra leading whitespace so the comments start at the same column as the `<section>` tag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/app/layout.tsx`:
- Around line 28-33: The twitter metadata block (twitter) and the openGraph
metadata block (openGraph) are missing image URLs so the card type
"summary_large_image" won't render a large image; update both the twitter object
and the openGraph object to include an images field (or image property) with a
fully-qualified image URL (and optional alt/type entries) pointing to your
social preview image so the summary_large_image card and OG previews render
correctly (e.g., add twitter.images = [{ url: "https://.../social-preview.png",
alt: "Perspective preview" }] and openGraph.images = [{ url: "...", alt: "..."
}]).
In `@frontend/app/page.tsx`:
- Around line 374-389: Remove the duplicate SoftwareApplication JSON-LD Script
block in page.tsx (the <Script id="software-schema" ...> block) so you don't
emit two conflicting SoftwareApplication schemas; delete that Script element
entirely and ensure any non-page-specific fields (name, url,
applicationCategory, description, operatingSystem) remain defined only in
layout.tsx's JSON-LD so all canonical app metadata is consolidated there.
---
Nitpick comments:
In `@frontend/app/layout.tsx`:
- Around line 56-70: The script uses dangerouslySetInnerHTML with JSON.stringify
(the JSON-LD payload in layout.tsx) which is currently static but triggers an
XSS concern; fix by sanitizing the stringified JSON before injecting—either use
a safe serializer like serialize-javascript to produce the JSON-LD or
post-process the JSON.stringify output to replace every "<" with its Unicode
escape (e.g., replace(/</g, '\\u003c')) so the value passed to
dangerouslySetInnerHTML (and produced by JSON.stringify in the same block)
cannot introduce script tags if the payload becomes dynamic.
- Line 24: The hardcoded site URL is duplicated in openGraph.url (layout.tsx),
the JSON-LD url field, and page.tsx; extract it into a single source (e.g.,
const SITE_URL) or read from process.env.NEXT_PUBLIC_SITE_URL and replace all
hardcoded occurrences. Update the openGraph object (openGraph.url), the JSON-LD
construction (the url field around line 67 in layout.tsx), and the usage in
page.tsx so they all reference SITE_URL (or the env-backed constant) instead of
the literal "https://perspective-aossie.vercel.app/". Ensure the constant is
exported/imported where needed so both layout.tsx and page.tsx use the same
value.
In `@frontend/app/page.tsx`:
- Around line 354-372: The FAQ JSON-LD is being rendered via next/script in the
client component (page.tsx with "use client") so it won't appear in SSR HTML;
move the FAQ structured-data block that builds the FAQPage from faqData into the
server component layout (layout.tsx) alongside the existing SoftwareApplication
JSON-LD, remove the next/Script usage from page.tsx, and render a plain <script
type="application/ld+json"> with JSON.stringify(...) of the same object in
layout.tsx so the FAQ JSON-LD is present in the initial server response; keep
faqData as-is since it is static.
- Around line 95-116: The faqData array is declared inside the component,
causing it to be re-created on every render and preventing reuse in server-level
JSON-LD; move the faqData variable to module scope (or a shared data file) so it
becomes a single exported constant that can be imported by the page component
and by any layout/server component that injects the JSON-LD; update references
in page.tsx to use the module-level faqData (or imported constant) and ensure
any JSON-LD generation uses the same exported constant to avoid duplication.
- Around line 301-302: Adjust the indentation of the inline JSX comments so they
match the surrounding JSX; specifically, align the `{/* FAQ Section */}` and
`{/* FAQ Structured Data */}` comments with the `<section className="container
mx-auto px-4 py-12 md:py-20">` element (and other sibling tags) by removing the
extra leading whitespace so the comments start at the same column as the
`<section>` tag.
| twitter: { | ||
| card: "summary_large_image", | ||
| title: "Perspective - AI-Powered Bias Detection", | ||
| description: | ||
| "AI-powered tool to analyze bias and generate alternative perspectives.", | ||
| }, |
There was a problem hiding this comment.
card: "summary_large_image" with no image URL will not render a large image card.
Twitter/X (and most Open Graph consumers) require an explicit image URL to display the summary_large_image card format. Without it, the card silently falls back to a plain text summary, defeating the purpose. The same applies to the openGraph block (lines 20–27) which also omits images.
♻️ Add image fields to both OG and Twitter metadata
openGraph: {
title: "Perspective - AI-Powered Bias Detection",
description: "Analyze content for bias and generate alternative AI-driven perspectives.",
url: "https://perspective-aossie.vercel.app/",
siteName: "Perspective",
type: "website",
+ images: [
+ {
+ url: "https://perspective-aossie.vercel.app/og-image.png",
+ width: 1200,
+ height: 630,
+ alt: "Perspective - AI-Powered Bias Detection",
+ },
+ ],
},
twitter: {
card: "summary_large_image",
title: "Perspective - AI-Powered Bias Detection",
description: "AI-powered tool to analyze bias and generate alternative perspectives.",
+ images: ["https://perspective-aossie.vercel.app/og-image.png"],
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/layout.tsx` around lines 28 - 33, The twitter metadata block
(twitter) and the openGraph metadata block (openGraph) are missing image URLs so
the card type "summary_large_image" won't render a large image; update both the
twitter object and the openGraph object to include an images field (or image
property) with a fully-qualified image URL (and optional alt/type entries)
pointing to your social preview image so the summary_large_image card and OG
previews render correctly (e.g., add twitter.images = [{ url:
"https://.../social-preview.png", alt: "Perspective preview" }] and
openGraph.images = [{ url: "...", alt: "..." }]).
| <Script | ||
| id="software-schema" | ||
| type="application/ld+json" | ||
| dangerouslySetInnerHTML={{ | ||
| __html: JSON.stringify({ | ||
| "@context": "https://schema.org", | ||
| "@type": "SoftwareApplication", | ||
| name: "Perspective", | ||
| applicationCategory: "ResearchApplication", | ||
| operatingSystem: "Web", | ||
| description: | ||
| "Perspective is an AI-powered research tool that analyzes online articles to detect bias and generate structured counter-perspectives.", | ||
| url: "https://perspective-aossie.vercel.app/", | ||
| }), | ||
| }} | ||
| /> |
There was a problem hiding this comment.
Duplicate SoftwareApplication JSON-LD block with a conflicting applicationCategory.
layout.tsx already injects a SoftwareApplication JSON-LD block (applicationCategory: "AIApplication"). This second block uses applicationCategory: "ResearchApplication" and a different description. Two SoftwareApplication schemas on the same page with conflicting properties is an SEO anti-pattern—search engines may ignore both or report a validation error.
Remove the SoftwareApplication Script block from page.tsx entirely and consolidate all non-page-specific structured data in layout.tsx.
🐛 Proposed fix
- <Script
- id="software-schema"
- type="application/ld+json"
- dangerouslySetInnerHTML={{
- __html: JSON.stringify({
- "@context": "https://schema.org",
- "@type": "SoftwareApplication",
- name: "Perspective",
- applicationCategory: "ResearchApplication",
- operatingSystem: "Web",
- description:
- "Perspective is an AI-powered research tool that analyzes online articles to detect bias and generate structured counter-perspectives.",
- url: "https://perspective-aossie.vercel.app/",
- }),
- }}
- />📝 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.
| <Script | |
| id="software-schema" | |
| type="application/ld+json" | |
| dangerouslySetInnerHTML={{ | |
| __html: JSON.stringify({ | |
| "@context": "https://schema.org", | |
| "@type": "SoftwareApplication", | |
| name: "Perspective", | |
| applicationCategory: "ResearchApplication", | |
| operatingSystem: "Web", | |
| description: | |
| "Perspective is an AI-powered research tool that analyzes online articles to detect bias and generate structured counter-perspectives.", | |
| url: "https://perspective-aossie.vercel.app/", | |
| }), | |
| }} | |
| /> |
🧰 Tools
🪛 ast-grep (0.40.5)
[warning] 376-376: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html
(react-unsafe-html-injection)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/app/page.tsx` around lines 374 - 389, Remove the duplicate
SoftwareApplication JSON-LD Script block in page.tsx (the <Script
id="software-schema" ...> block) so you don't emit two conflicting
SoftwareApplication schemas; delete that Script element entirely and ensure any
non-page-specific fields (name, url, applicationCategory, description,
operatingSystem) remain defined only in layout.tsx's JSON-LD so all canonical
app metadata is consolidated there.
Description
This PR enhances SEO for the Perspective landing page by adding structured data using JSON-LD schema.
issue
#141
Demo video
Screen.Recording.2026-02-21.235518.mp4
Changes Made
Why This Change?
No UI or functional changes were introduced.
Type of Change
Summary by CodeRabbit