Skip to content

feat: add structured data (FAQPage & SoftwareApplication) for improved SEO#142

Open
charuljain02 wants to merge 2 commits intoAOSSIE-Org:mainfrom
charuljain02:feat/add-seo-structured-data
Open

feat: add structured data (FAQPage & SoftwareApplication) for improved SEO#142
charuljain02 wants to merge 2 commits intoAOSSIE-Org:mainfrom
charuljain02:feat/add-seo-structured-data

Conversation

@charuljain02
Copy link

@charuljain02 charuljain02 commented Feb 21, 2026

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

  • Added FAQ section structured data using FAQPage schema
  • Added SoftwareApplication schema for better search engine visibility
  • Ensured structured data dynamically maps from existing FAQ data
  • Preserved UI and existing layout

Why This Change?

  • Improves search engine understanding of the application
  • Enables rich results for FAQ in search
  • Enhances discoverability and indexing

No UI or functional changes were introduced.

Type of Change

  • Enhancement
  • Bug fix
  • Breaking change

Summary by CodeRabbit

  • New Features
    • Added a frequently asked questions (FAQ) section featuring multiple Q&A pairs.
    • Extended page metadata with keywords and social media information.
    • Enhanced structured data markup on the page.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

SEO 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

Cohort / File(s) Summary
Root Layout SEO & Metadata
frontend/app/layout.tsx
Extended metadata object with description, keywords array, openGraph, and twitter properties. Added JSON-LD SoftwareApplication structured data block for search engine optimization.
Page FAQ & Structured Data
frontend/app/page.tsx
Introduced faqData structure with four Q&A pairs. Added FAQ section rendering with cards. Injected JSON-LD FAQPage and SoftwareApplication structured data blocks via Script components.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Hops through metadata fields with glee,
FAQ questions bloom like clover spree,
JSON-LD whispers to search engines kind,
SEO treasures, structured and refined—
A rabbit's work makes the website shine! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding structured data (FAQPage & SoftwareApplication) for SEO, which is clearly reflected in both file changes with JSON-LD implementations.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
frontend/app/layout.tsx (2)

56-70: dangerouslySetInnerHTML with JSON.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.stringify does not sanitize malicious strings for XSS injection, and recommends replacing < with its Unicode equivalent \u003c, or using a community alternative such as serialize-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 across layout.tsx and page.tsx.

"https://perspective-aossie.vercel.app/" appears in three places: openGraph.url (line 24), the JSON-LD url field (line 67), and page.tsx line 386. A typo or domain migration will silently break structured data and OG tags. Extract it to a shared constant or read from process.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_URL in layout.tsx and page.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 via next/script (afterInteractive) is absent from the initial SSR HTML.

afterInteractive scripts 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 in layout.js or page.js components (a plain <script> tag, not next/script).

Because page.tsx is "use client", a raw <script> tag can't be rendered here without risking hydration mismatches. The cleanest fix is to move the FAQPage JSON-LD into layout.tsx (a server component) alongside the existing SoftwareApplication block, since faqData is 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: faqData is suitable as a module-level constant or shared data file.

Since faqData is 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.

Comment on lines +28 to +33
twitter: {
card: "summary_large_image",
title: "Perspective - AI-Powered Bias Detection",
description:
"AI-powered tool to analyze bias and generate alternative perspectives.",
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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: "..." }]).

Comment on lines +374 to +389
<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/",
}),
}}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant