Skip to content

Conversation

@goldflag
Copy link
Collaborator

@goldflag goldflag commented Nov 23, 2025

…cture

  • Added new tool pages to the sitemap, including UTM Builder, SEO Title Generator, and Privacy Policy Builder, enhancing SEO and discoverability.
  • Updated the sitemap function to return a comprehensive list of tool pages alongside existing static and dynamic pages.
  • Refactored the layout of the sitemap for better organization and maintainability.

Summary by CodeRabbit

  • New Features

    • Added breadcrumb navigation, a Related Tools section, a unified tool layout with CTA, and a set of interactive marketing/SEO tools (analytics detector, bounce rate, CTR, funnel visualizer, ROI, meta descriptions, OG tags, page speed, privacy policy builder, sample size, SEO title, traffic value, UTM builder).
  • Bug Fixes

    • Removed the AI Privacy Policy Generator tool entry from the catalog.
  • Improvements

    • Enhanced SEO metadata and JSON-LD across tool pages; sitemap now includes tool pages and updated ordering; simplified page styling by removing background gradients.
  • Documentation

    • Added repository usage/contribution guidance and a tool migration guide.

✏️ Tip: You can customize this high-level summary in your review settings.

…cture

- Added new tool pages to the sitemap, including UTM Builder, SEO Title Generator, and Privacy Policy Builder, enhancing SEO and discoverability.
- Updated the sitemap function to return a comprehensive list of tool pages alongside existing static and dynamic pages.
- Refactored the layout of the sitemap for better organization and maintainability.
@vercel
Copy link

vercel bot commented Nov 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
rybbit Ready Ready Preview Comment Nov 23, 2025 10:44pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

Warning

Rate limit exceeded

@goldflag has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 26 minutes and 3 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📥 Commits

Reviewing files that changed from the base of the PR and between 57e0c23 and 8f18d78.

📒 Files selected for processing (15)
  • docs/src/app/(home)/tools/CLAUDE.md (1 hunks)
  • docs/src/app/(home)/tools/analytics-detector/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/meta-description-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/og-tag-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/page-speed-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/privacy-policy-builder/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/seo-title-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/page.tsx (1 hunks)

Walkthrough

Many interactive tool UIs were extracted into standalone client form components and reused by new ToolPageLayout-based pages which add metadata and JSON‑LD; Breadcrumbs, RelatedTools, ToolCTA, sitemap, and several docs were added; the AI Privacy Policy Generator page was removed and minor styling/gits changes applied.

Changes

Cohort / File(s) Summary
New tool form components
docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx, docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx, docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx, docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx, docs/src/app/(home)/tools/marketing-roi-calculator/MarketingROIForm.tsx, docs/src/app/(home)/tools/meta-description-generator/MetaDescriptionForm.tsx, docs/src/app/(home)/tools/og-tag-generator/OGTagForm.tsx, docs/src/app/(home)/tools/page-speed-calculator/PageSpeedForm.tsx, docs/src/app/(home)/tools/privacy-policy-builder/PrivacyPolicyBuilderForm.tsx, docs/src/app/(home)/tools/sample-size-calculator/SampleSizeForm.tsx, docs/src/app/(home)/tools/seo-title-generator/SEOTitleForm.tsx, docs/src/app/(home)/tools/traffic-value-calculator/TrafficValueForm.tsx, docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx
Added exported client React components implementing each tool's interactive UI, local state, validation/calculation or API flows, result rendering, copy/download/clear actions, and FAQ/help UI.
Tool page refactors
docs/src/app/(home)/tools/.../page.tsx (analytics-detector, bounce-rate-calculator, ctr-calculator, funnel-visualizer, marketing-roi-calculator, meta-description-generator, og-tag-generator, page-speed-calculator, privacy-policy-builder, sample-size-calculator, seo-title-generator, traffic-value-calculator, utm-builder)
Rewrote pages to use ToolPageLayout and the new form components, added/exported metadata, injected JSON‑LD structuredData (WebApplication/FAQ), moved content into educationalContent/faqs props, and swapped inline CTA/state/API logic for composed components.
Tool layout & helpers
docs/src/app/(home)/tools/components/ToolPageLayout.tsx, docs/src/app/(home)/tools/components/index.ts
Added ToolPageLayout with ToolPageLayoutProps and FAQItem interfaces; created a barrel export re-exporting ToolCTA and ToolPageLayout plus type exports.
Navigation & CTA components
docs/src/components/Breadcrumbs.tsx, docs/src/components/RelatedTools.tsx, docs/src/app/(home)/tools/components/ToolCTA.tsx
Added Breadcrumbs, RelatedTools (static tools list + filtering), and ToolCTA (signup CTA wired to TrackedButton/DEFAULT_EVENT_LIMIT).
Tool removal & styling tweaks
docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (deleted), docs/src/app/(home)/oss-friends/page.tsx, docs/src/app/(home)/tools/page.tsx
Deleted AI Privacy Policy Generator page; removed background gradient classes from OSS Friends and Tools page containers (kept min-h-screen); removed the tool entry from the tools listing.
Sitemap & blog changes
docs/src/app/sitemap.ts, docs/src/app/blog/[...slug]/page.tsx
Sitemap now dynamically enumerates /tools/{slug} and adds a /tools entry; blog canonical URL changed from https://docs.rybbit.io/blog/{slug} to https://rybbit.com/blog/{slug} (plus minor formatting tweak).
Docs & tooling files
CLAUDE.md, docs/CLAUDE.md, .gitignore, docs/src/app/(home)/tools/MIGRATION_GUIDE.md
Added CLAUDE.md and docs/CLAUDE.md, added a Tool Pages Migration Guide, and removed CLAUDE.md from .gitignore while keeping .claude/ ignored.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ToolPage
    participant ToolPageLayout
    participant FormComponent
    participant API

    User->>ToolPage: Navigate to /tools/{slug}
    ToolPage->>ToolPageLayout: Render layout + structuredData
    ToolPageLayout->>ToolPageLayout: Render Breadcrumbs, Header, RelatedTools, CTA
    ToolPageLayout->>FormComponent: Mount tool component
    User->>FormComponent: Provide inputs / Submit
    FormComponent->>API: POST /api/tools/{endpoint} (if applicable)
    API-->>FormComponent: Response (+ X-RateLimit-Remaining)
    alt success
        FormComponent->>User: Show results (cards/charts/text)
    else failure
        FormComponent->>User: Show error alert
    end
    User->>FormComponent: Copy / Download / Clear
    FormComponent->>User: Update UI feedback
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Files/areas to focus review on:
    • Consistent API error handling and parsing of X-RateLimit-Remaining across form components.
    • Accuracy and completeness of per-page metadata and JSON‑LD structuredData.
    • RelatedTools filtering logic and accessibility/semantics of Breadcrumbs.
    • Ensure removal of the AI privacy tool has no remaining references (navigation lists, imports, sitemap generation edge cases).

Possibly related PRs

Poem

🐇
I hopped through files with eager paws,
Split tools to forms and fixed their flaws,
Breadcrumbs mark the pathways clear,
Related tools bring friends near,
Metadata hums — the docs applause!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'add free tools' is vague and does not clearly convey the main changes, which involve comprehensive refactoring of tool pages and sitemap enhancements. Consider a more descriptive title such as 'Refactor tool pages to use ToolPageLayout and update sitemap generation' to better reflect the scope and primary changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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: 9

🧹 Nitpick comments (28)
docs/src/app/sitemap.ts (1)

24-40: Consider dynamically generating the tool list to reduce maintenance burden.

Verification confirms all 14 hardcoded tool slugs have corresponding page files at docs/src/app/(home)/tools/{slug}/page.tsx, and no orphaned tools exist. However, the hardcoding creates a maintenance anti-pattern: any new tool must be manually added to this array or it won't appear in the sitemap, risking missed SEO benefits.

Dynamic discovery would eliminate this risk and ensure new tools are automatically included.

docs/src/app/(home)/tools/meta-description-generator/MetaDescriptionForm.tsx (1)

57-61: Add error handling for clipboard operations.

The navigator.clipboard.writeText() call can fail if the user denies permission or if the page is not in a secure context (HTTPS). Consider adding a try-catch block to handle failures gracefully.

Apply this diff:

  const copyDescription = async (description: string, index: number) => {
-   await navigator.clipboard.writeText(description);
-   setCopiedIndex(index);
-   setTimeout(() => setCopiedIndex(null), 2000);
+   try {
+     await navigator.clipboard.writeText(description);
+     setCopiedIndex(index);
+     setTimeout(() => setCopiedIndex(null), 2000);
+   } catch (err) {
+     // Fallback: could show an error message or use a different copy method
+     console.error('Failed to copy to clipboard:', err);
+   }
  };
docs/src/app/(home)/tools/privacy-policy-builder/PrivacyPolicyBuilderForm.tsx (1)

129-135: Add error handling for clipboard operations.

Similar to other tools in this PR, the clipboard operation lacks error handling. Consider wrapping it in a try-catch block.

Apply this diff:

  const copyToClipboard = async () => {
    if (policy) {
-     await navigator.clipboard.writeText(policy);
-     setCopied(true);
-     setTimeout(() => setCopied(false), 2000);
+     try {
+       await navigator.clipboard.writeText(policy);
+       setCopied(true);
+       setTimeout(() => setCopied(false), 2000);
+     } catch (err) {
+       console.error('Failed to copy to clipboard:', err);
+     }
    }
  };
docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (1)

7-18: Consider renaming interfaces to avoid naming collision.

The FunnelStep and FunnelChartData types defined here have the same names as types in client/src/api/analytics/funnels/useGetFunnel.ts, but with completely different structures. While they're in different packages (docs vs. client), this creates confusion and makes the codebase harder to maintain.

Consider prefixing these with a namespace or descriptive qualifier:

-interface FunnelStep {
+interface ToolFunnelStep {
   name: string;
   visitors: string;
 }

-interface FunnelChartData {
+interface ToolFunnelChartData {
   stepName: string;
   visitors: number;
   conversionRate: number;
   dropoffRate: number;
   stepNumber: number;
 }

Then update the usages:

-const [steps, setSteps] = useState<FunnelStep[]>([
+const [steps, setSteps] = useState<ToolFunnelStep[]>([
   // ...
 ]);

-const calculateFunnelData = (): FunnelChartData[] | null => {
+const calculateFunnelData = (): ToolFunnelChartData[] | null => {
   // ...
 };
docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx (1)

22-27: Handle 0 bounced sessions and numeric validation more explicitly

calculateBounceRate treats 0 as falsy, so a valid case like bouncedSessions = "0" never shows a 0% bounce rate, and negative values aren’t rejected explicitly.

Consider guarding for NaN/bounds instead of generic falsy checks:

-  const calculateBounceRate = () => {
-    const total = parseFloat(totalSessions);
-    const bounced = parseFloat(bouncedSessions);
-    if (!total || !bounced || total === 0) return null;
-    return (bounced / total) * 100;
-  };
+  const calculateBounceRate = () => {
+    const total = parseFloat(totalSessions);
+    const bounced = parseFloat(bouncedSessions);
+
+    if (
+      Number.isNaN(total) ||
+      Number.isNaN(bounced) ||
+      total <= 0 ||
+      bounced < 0 ||
+      bounced > total
+    ) {
+      return null;
+    }
+
+    return (bounced / total) * 100;
+  };
docs/src/components/RelatedTools.tsx (1)

11-115: Related tools logic looks solid; consider future data extraction

Filtering out the current tool, reordering by category, and slicing by maxTools is clean and easy to follow. If this catalog grows, you may eventually want to move allTools into a shared config/data module so non-UI changes (adding tools, categories) don’t require touching this component.

docs/src/app/(home)/tools/ai-privacy-policy-generator/PrivacyPolicyForm.tsx (1)

56-60: Harden clipboard copy against API failures

copyToClipboard assumes navigator.clipboard.writeText always exists and succeeds; in older browsers or restricted contexts this can reject, causing an unhandled promise.

Wrapping in try/catch and surfacing an error (or silently no-op) would make this more robust:

-  const copyToClipboard = async () => {
-    await navigator.clipboard.writeText(generatedPolicy);
-    setCopied(true);
-    setTimeout(() => setCopied(false), 2000);
-  };
+  const copyToClipboard = async () => {
+    try {
+      await navigator.clipboard.writeText(generatedPolicy);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } catch {
+      // Optionally: setError("Unable to copy to clipboard");
+    }
+  };
docs/src/app/(home)/tools/traffic-value-calculator/TrafficValueForm.tsx (1)

13-27: Tighten numeric validation and reuse validated inputs for calculations

calculateValue currently uses falsy checks, so 0 values (e.g. 0% conversion rate) are treated as invalid, while negative numbers pass through. The 1% improvement calculation also reparses raw state rather than reusing validated inputs.

You can make this more robust and self-consistent by explicitly checking for NaN and bounds:

-  const calculateValue = () => {
-    const visitors = parseFloat(monthlyVisitors);
-    const cr = parseFloat(conversionRate) / 100;
-    const aov = parseFloat(averageOrderValue);
-    const margin = parseFloat(profitMargin) / 100;
-
-    if (!visitors || !cr || !aov || !margin) return null;
+  const calculateValue = () => {
+    const visitors = parseFloat(monthlyVisitors);
+    const crPercent = parseFloat(conversionRate);
+    const aov = parseFloat(averageOrderValue);
+    const marginPercent = parseFloat(profitMargin);
+
+    if (
+      Number.isNaN(visitors) ||
+      Number.isNaN(crPercent) ||
+      Number.isNaN(aov) ||
+      Number.isNaN(marginPercent) ||
+      visitors <= 0 ||
+      crPercent < 0 ||
+      aov <= 0 ||
+      marginPercent < 0
+    ) {
+      return null;
+    }
+
+    const cr = crPercent / 100;
+    const margin = marginPercent / 100;
@@
-                  <li>
-                    • A <strong>1% conversion rate improvement</strong> would add{" "}
-                    <strong>
-                      $
-                      {(
-                        (parseFloat(monthlyVisitors) *
-                          0.01 *
-                          parseFloat(averageOrderValue) *
-                          parseFloat(profitMargin)) /
-                        100
-                      ).toLocaleString(undefined, { maximumFractionDigits: 0 })}
-                      /month
-                    </strong>{" "}
-                    in profit
-                  </li>
+                  <li>
+                    • A <strong>1% conversion rate improvement</strong> would add{" "}
+                    <strong>
+                      $
+                      {(
+                        visitors * 0.01 * aov * margin
+                      ).toLocaleString(undefined, { maximumFractionDigits: 0 })}
+                      /month
+                    </strong>{" "}
+                    in profit
+                  </li>

This preserves behavior for valid inputs, allows zero-rate scenarios to render as zero value, and keeps all calculations based on the same validated numbers.

Also applies to: 196-205

docs/src/app/(home)/tools/marketing-roi-calculator/MarketingROIForm.tsx (1)

17-29: Use explicit numeric validation instead of generic falsy checks in calculateMetrics

if (!spend || !rev || spend === 0) hides legitimate cases like revenue = 0 (which could still have a well-defined, very negative ROI) while allowing negative inputs to slip through.

You can make validation clearer and less error-prone by checking for NaN and bounds directly:

-  const calculateMetrics = (): Metrics | null => {
-    const spend = parseFloat(adSpend);
-    const rev = parseFloat(revenue);
-    const cogs = parseFloat(costOfGoodsSold) || 0;
-
-    if (!spend || !rev || spend === 0) return null;
+  const calculateMetrics = (): Metrics | null => {
+    const spend = parseFloat(adSpend);
+    const rev = parseFloat(revenue);
+    const cogs = parseFloat(costOfGoodsSold) || 0;
+
+    if (
+      Number.isNaN(spend) ||
+      Number.isNaN(rev) ||
+      spend <= 0 ||
+      rev <= 0
+    ) {
+      return null;
+    }

This keeps the downstream math unchanged while making the accepted input domain explicit.

docs/src/app/(home)/tools/ctr-calculator/page.tsx (2)

45-113: Align JSON-LD FAQ entries with visible FAQ content

The FAQPage.mainEntity includes questions/answers (e.g., “Why is my CTR different from competitor benchmarks?”) that don’t appear as visible Q&A on the page, while some visible FAQ items aren’t in the JSON-LD. For better compliance with search guidelines and to avoid confusing crawlers, consider either:

  • mirroring the visible FAQ items exactly in structuredData, or
  • adding corresponding visible Q&A content for every JSON-LD FAQ entry.

Also applies to: 398-465


115-119: JSON-LD script uses dangerouslySetInnerHTML (lint noise vs. practical need)

Using dangerouslySetInnerHTML here is the standard way to emit JSON-LD and is safe as long as structuredData stays fully static / not user‑influenced. If you want to appease the security lints, consider either:

  • centralizing this into a small helper component where the lint rule is locally disabled, or
  • adding an inline rule suppression just for this script tag with a brief comment that the data is static JSON-LD.
docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)

17-31: Improve feedback for invalid URLs and clipboard failures

Right now, if new URL(...) throws you just return "", so the “Your UTM URL” section never appears and the user gets no explanation. Similarly, a rejected navigator.clipboard.writeText will throw and never toggle copied.

Consider:

  • Tracking a small error state to surface “Please enter a valid URL” when the try block fails.
  • Wrapping copyToClipboard in a try/catch and optionally showing a brief error/toast if clipboard access fails (or at least swallowing the error to avoid noisy console logs).

These are UX polish items, but they’ll make the tool more self‑explanatory.

Also applies to: 33-39, 141-161

docs/src/app/(home)/tools/seo-title-generator/SEOTitleForm.tsx (1)

24-33: Tighten validation and clipboard robustness for title generation

Minor UX improvements you might consider:

  • Disable the “Generate SEO Titles” button when topic is empty (e.g., disabled={isLoading || !topic.trim()}) so the user can’t trigger a no‑op request and only then see an error.
  • As with the other forms, wrap navigator.clipboard.writeText in a try/catch to avoid unhandled rejections if clipboard access fails.

These changes are optional but would make the interaction a bit more resilient.

Also applies to: 58-62, 117-130

docs/src/app/(home)/tools/sample-size-calculator/page.tsx (2)

1-12: Use import type for Metadata and double‑check canonical domain

To keep Metadata strictly type‑only and match the pattern in other pages (e.g., the CTR calculator), consider:

-import { Metadata } from "next";
+import type { Metadata } from "next";

Also, this page lives under the docs app tree but sets canonical/OpenGraph URLs to https://rybbit.com/tools/sample-size-calculator. If that’s intentionally canonicalizing docs → main marketing domain, all good; otherwise it’s worth verifying the target domain is correct.


124-128: JSON-LD script uses dangerouslySetInnerHTML (same pattern as CTR page)

As with the CTR page, using dangerouslySetInnerHTML for static JSON-LD is the standard approach and safe as long as structuredData remains hard‑coded and not user‑derived. If you want to quiet the security lints, you can:

  • isolate this into a tiny JSON-LD helper component with a local lint suppression, or
  • add a one-line suppression + comment explaining the data is static.
docs/src/app/(home)/tools/analytics-detector/page.tsx (1)

43-116: Handle dangerouslySetInnerHTML + lint warnings via a shared JSON‑LD helper or inline ignore

The JSON‑LD is static and not user‑controlled, so using <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> is the standard pattern here, but Biome flags it. Consider either:

  • Extracting a <JsonLd data={jsonLd} /> helper that encapsulates this pattern and carries a one‑time /* biome-ignore lint/security/noDangerouslySetInnerHtml */, or
  • Adding that ignore comment directly above this <script> tag.

That keeps XSS risk low while satisfying lint rules across all tool pages using the same approach.

docs/src/app/(home)/tools/utm-builder/page.tsx (1)

43-115: Refactor JSON‑LD injection to central helper or annotate for Biome

As with the analytics detector page, this uses the standard JSON‑LD pattern with dangerouslySetInnerHTML and static structuredData. To keep Biome happy and centralize XSS considerations, you could:

// Example helper component
/* biome-ignore lint/security/noDangerouslySetInnerHtml */
function JsonLd({ data }: { data: unknown }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

and then use <JsonLd data={structuredData} /> here and on other tool pages.

docs/src/app/(home)/tools/og-tag-generator/OGTagForm.tsx (1)

60-64: Guard against clipboard API failures in copyCode

navigator.clipboard.writeText can fail (HTTP not HTTPS, browser restrictions, denied permissions), which would currently result in an unhandled promise rejection. Consider wrapping this in try/catch and optionally surfacing an error message:

const copyCode = async (code: string, index: number) => {
  try {
    await navigator.clipboard.writeText(code);
    setCopiedIndex(index);
    setTimeout(() => setCopiedIndex(null), 2000);
  } catch {
    setError("Unable to copy to clipboard. Please copy manually.");
  }
};

This makes the UX more robust on stricter environments.

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1)

45-117: Apply the shared JSON‑LD helper or lint ignore here as well

This page repeats the same static JSON‑LD + dangerouslySetInnerHTML pattern. Once you introduce a shared <JsonLd /> helper or add a Biome ignore near this script, please reuse that approach here to keep lint output clean and centralize any future security review.

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1)

42-113: Unify JSON‑LD injection approach and address dangerouslySetInnerHTML lint

As with the other tool pages, this uses static structured data plus dangerouslySetInnerHTML. To avoid repeated Biome warnings while keeping this standard pattern, please either:

  • Use a shared <JsonLd data={structuredData} /> helper that encapsulates the script tag and carries a targeted /* biome-ignore lint/security/noDangerouslySetInnerHtml */, or
  • Add that ignore comment near this script and document that only static, non‑user data is injected.

That keeps things explicit from a security-review standpoint.

docs/src/app/(home)/tools/page-speed-calculator/page.tsx (1)

48-120: JSON-LD injection via dangerouslySetInnerHTML is safe here but will keep lint failing

You’re stringifying a fully static schema object and injecting it as JSON-LD. Since there’s no user input, practical XSS risk is negligible, but the security lints will continue to complain.

Two options:

  • Keep as-is and explicitly disable/adjust the noDangerouslySetInnerHtml rule for this line/pattern, or
  • Extract a small <JsonLd data={schema} /> helper (or use next/script) where you centralize the dangerouslySetInnerHTML usage and lint override.

Either way, note that any future changes must keep schema free of user-controlled data.

docs/src/app/(home)/tools/privacy-policy-builder/page.tsx (1)

40-115: Static JSON-LD + dangerouslySetInnerHTML: safe pattern but triggers security lint

As with the other tools, structuredData is a static object and is only serialized via JSON.stringify, so there’s no realistic XSS path. However, linters flag the direct dangerouslySetInnerHTML usage.

Consider extracting a shared <JsonLd data={...} /> (or next/script wrapper) where you locally disable the lint, and reuse that across tool pages; or document the exception and suppress the rule just on this line.

docs/src/app/(home)/tools/meta-description-generator/page.tsx (1)

38-115: Repeat JSON-LD pattern: static data is fine; centralize or lint-suppress

Same pattern as other tool pages: static structuredData serialized into a JSON-LD <script> via dangerouslySetInnerHTML. This is effectively safe given the lack of user input, but it will keep tripping security linters.

I’d suggest either:

  • Reusing a shared <JsonLd> helper that encapsulates the dangerouslySetInnerHTML usage (and any lint suppression), or
  • Adding a targeted lint disable around this line and documenting that only static objects may be passed.

Also ensure the canonical URL (https://rybbit.com/tools/meta-description-generator) matches whatever domain you decide on for all tools, as noted on the privacy-policy-builder review.

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1)

46-127: Static JSON-LD script again safe, but centralization would reduce lint noise

structuredData is a compile-time object, and the JSON-LD <script> uses JSON.stringify, so there’s no real XSS surface here. Nevertheless, Biome and ast-grep will keep flagging dangerouslySetInnerHTML.

Same suggestion as other files: centralize this into a reusable helper (or next/script wrapper) with a documented lint override, or apply a local lint suppression and clearly state that only static data should be passed.

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)

41-113: JSON-LD schema inside component: safe, but consider hoisting/centralizing

schemaData is static and only used to render a JSON-LD <script> via dangerouslySetInnerHTML. That’s safe from XSS here, but:

  • You can hoist schemaData to module scope to avoid re-allocating it per render.
  • As with other pages, you may want a shared <JsonLd data={schemaData} /> (or equivalent) to encapsulate the dangerouslySetInnerHTML usage and handle lint suppression in one place.

This would reduce duplication and keep security tooling happy.

docs/src/app/(home)/tools/og-tag-generator/page.tsx (2)

68-110: Duplicated FAQ content between JSON‑LD and visible Accordion

The FAQ Q&A content is duplicated in both structuredData (FAQPage) and the Accordion UI. This is fine functionally, but it does create a maintenance hotspot: any future copy edits will need to be applied in two places and it’s easy for them to drift.

If you expect to iterate on this content, consider centralizing the FAQ data in a shared array and:

  • Map it into structuredData.@graph[1].mainEntity, and
  • Map it again into the Accordion items.

That keeps JSON‑LD and visible content in sync with a single source of truth.

Also applies to: 395-461


333-345: Consider pointing to LinkedIn’s official “Post Inspector” tool

The copy refers to LinkedIn’s Post Inspector but the example URLs and description lean toward general feed usage. To avoid confusing users, you might want to either:

  • Link directly to LinkedIn’s official Post Inspector endpoint, or
  • Rephrase to clearly distinguish “test via the feed preview” vs using a dedicated inspector tool.

This is content‑only, but adjusting it would make the testing guidance clearer.

Also applies to: 424-437

docs/src/app/(home)/tools/seo-title-generator/page.tsx (1)

62-104: Shared FAQ content for schema and UI could be data‑driven

The same FAQ questions/answers are encoded twice: once in structuredData (FAQPage) and once in the Accordion. That’s OK now, but it’s easy for them to diverge over time.

If you anticipate copy changes, consider extracting a shared data structure like:

const faqItems = [
  {
    id: "item-1",
    question: "What makes a good SEO title?",
    answer:
      "A good SEO title is 50-60 characters, includes your primary keyword, accurately describes the page content, and is compelling enough to attract clicks. Front-load important keywords and make it unique for each page.",
  },
  // ...
];

Then:

  • Map faqItems into structuredData.@graph[1].mainEntity, and
  • Map faqItems into the Accordion JSX.

That keeps JSON‑LD and visible FAQs in sync with one source of truth.

Also applies to: 226-290

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b5e44f and 68ed61a.

📒 Files selected for processing (33)
  • docs/src/app/(home)/oss-friends/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/ai-privacy-policy-generator/PrivacyPolicyForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/analytics-detector/page.tsx (4 hunks)
  • docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/marketing-roi-calculator/MarketingROIForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/meta-description-generator/MetaDescriptionForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/meta-description-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/og-tag-generator/OGTagForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/og-tag-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/page-speed-calculator/PageSpeedForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/page-speed-calculator/page.tsx (2 hunks)
  • docs/src/app/(home)/tools/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/privacy-policy-builder/PrivacyPolicyBuilderForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/privacy-policy-builder/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/sample-size-calculator/SampleSizeForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/seo-title-generator/SEOTitleForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/seo-title-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/traffic-value-calculator/TrafficValueForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/page.tsx (1 hunks)
  • docs/src/app/sitemap.ts (3 hunks)
  • docs/src/components/Breadcrumbs.tsx (1 hunks)
  • docs/src/components/RelatedTools.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (16)
docs/src/app/(home)/tools/traffic-value-calculator/TrafficValueForm.tsx (1)
docs/src/components/ui/accordion.tsx (4)
  • Accordion (58-58)
  • AccordionItem (58-58)
  • AccordionTrigger (58-58)
  • AccordionContent (58-58)
docs/src/app/(home)/tools/seo-title-generator/SEOTitleForm.tsx (1)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx (1)
docs/src/components/ui/accordion.tsx (4)
  • Accordion (58-58)
  • AccordionItem (58-58)
  • AccordionTrigger (58-58)
  • AccordionContent (58-58)
docs/src/app/(home)/tools/privacy-policy-builder/PrivacyPolicyBuilderForm.tsx (1)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (2)
client/src/api/analytics/funnels/useGetFunnel.ts (1)
  • FunnelStep (8-15)
client/src/app/[site]/funnels/components/Funnel.tsx (1)
  • FunnelChartData (13-19)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx (2)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (1)
  • FunnelVisualizerForm (20-230)
docs/src/app/(home)/tools/privacy-policy-builder/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/privacy-policy-builder/PrivacyPolicyBuilderForm.tsx (1)
  • PrivacyPolicyBuilderForm (7-335)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/app/(home)/tools/meta-description-generator/MetaDescriptionForm.tsx (1)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (4)
docs/src/app/(home)/tools/page.tsx (1)
  • metadata (18-22)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx (1)
  • BounceRateForm (17-233)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/app/(home)/tools/analytics-detector/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx (1)
  • AnalyticsDetectorForm (19-182)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/ai-privacy-policy-generator/PrivacyPolicyForm.tsx (1)
  • PrivacyPolicyForm (6-198)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/page-speed-calculator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/page-speed-calculator/PageSpeedForm.tsx (1)
  • PageSpeedForm (22-365)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)
docs/src/components/ui/accordion.tsx (4)
  • Accordion (58-58)
  • AccordionItem (58-58)
  • AccordionTrigger (58-58)
  • AccordionContent (58-58)
docs/src/app/(home)/tools/ctr-calculator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx (1)
  • CTRCalculatorForm (15-158)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
docs/src/app/(home)/tools/meta-description-generator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/meta-description-generator/MetaDescriptionForm.tsx (1)
  • MetaDescriptionForm (14-198)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-160)
🪛 ast-grep (0.40.0)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[warning] 116-116: 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)

docs/src/app/(home)/tools/utm-builder/page.tsx

[warning] 113-113: 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)

docs/src/app/(home)/tools/privacy-policy-builder/page.tsx

[warning] 114-114: 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)

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx

[warning] 112-112: 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)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[warning] 117-117: 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)

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx

[warning] 112-112: 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)

docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[warning] 126-126: 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)

docs/src/app/(home)/tools/analytics-detector/page.tsx

[warning] 114-114: 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)

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx

[warning] 126-126: 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)

docs/src/app/(home)/tools/page-speed-calculator/page.tsx

[warning] 118-118: 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)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[warning] 117-117: 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)

docs/src/app/(home)/tools/seo-title-generator/page.tsx

[warning] 111-111: 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)

docs/src/app/(home)/tools/meta-description-generator/page.tsx

[warning] 113-113: 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)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[warning] 115-115: 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)

🪛 Biome (2.1.2)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[error] 117-117: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/utm-builder/page.tsx

[error] 114-114: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/privacy-policy-builder/page.tsx

[error] 115-115: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx

[error] 113-113: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[error] 118-118: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx

[error] 113-113: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[error] 127-127: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/analytics-detector/page.tsx

[error] 115-115: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx

[error] 127-127: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/page-speed-calculator/page.tsx

[error] 119-119: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[error] 118-118: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/seo-title-generator/page.tsx

[error] 112-112: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/meta-description-generator/page.tsx

[error] 114-114: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[error] 116-116: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)
🔇 Additional comments (32)
docs/src/app/(home)/oss-friends/page.tsx (1)

42-42: Verify styling consistency with broader refactoring pattern.

The background gradient classes have been removed from the page container. While this is a minor styling change and doesn't affect functionality, consider whether this aligns with the broader design direction across the site.

Additionally, based on the PR's focus on enhancing SEO and discoverability, this page may benefit from the same treatment as other refactored tool pages (Breadcrumbs, RelatedTools, and JSON-LD structured data), though it's unclear if this page is intended to be part of that scope.

docs/src/app/sitemap.ts (3)

69-74: Good priority hierarchy for the tools index page.

The tools index page at priority 0.85 appropriately ranks above individual tool pages at 0.8, which helps search engines understand the site structure.

Note: The weekly change frequency assumes tools are added/updated weekly. If updates are less frequent, consider monthly to match the individual tool pages.


83-83: LGTM! Clean sitemap structure.

The return order logically groups static pages, tool pages, documentation, and blog posts. This maintains good organization for the sitemap.


1-1: No action required.

Next.js 16.0.0 was released on October 21, 2025, and the code correctly uses this valid version. The original review comment's concern about a version discrepancy is incorrect. While the latest patch is 16.0.3, the use of 16.0.0 is not problematic.

Likely an incorrect or invalid review comment.

docs/src/components/Breadcrumbs.tsx (1)

1-48: LGTM! Well-structured breadcrumb component.

The component properly implements WCAG breadcrumb navigation with appropriate ARIA attributes, conditional link rendering, and visual separators. Using index as key is acceptable here since breadcrumb items are static and won't be reordered.

docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx (1)

1-158: LGTM! Clean CTR calculator implementation.

The component correctly calculates CTR, handles edge cases (division by zero), and provides meaningful benchmark comparisons. The logic is sound and the UI feedback is contextual.

docs/src/app/(home)/tools/page.tsx (1)

1-177: LGTM! Clean tools listing page.

The page properly exports metadata for SEO, uses Next.js Link for optimal client-side navigation, and provides a well-structured grid layout for tool discovery. The implementation is straightforward and follows Next.js best practices.

docs/src/app/(home)/tools/sample-size-calculator/SampleSizeForm.tsx (1)

11-33: LGTM! Statistically sound sample size calculation.

The implementation correctly applies the two-proportion sample size formula with appropriate Z-scores for the selected confidence levels and statistical power. Input validation and edge case handling are properly implemented.

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1)

45-48: JSON-LD injection via dangerouslySetInnerHTML is acceptable here but deserves a comment

Static analysis flags dangerouslySetInnerHTML, but in this case schemaData is a static object and is serialized with JSON.stringify, with no user input. That makes this a standard, low-risk pattern for JSON-LD.

To make this intent clear (and keep linters quiet), consider adding a brief comment and/or a localized rule suppression:

-    <>
-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }} />
+    <>
+      {/* Safe JSON-LD injection: schemaData is static and only serialized via JSON.stringify */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
+      />

If your tooling supports it, you can also add an inline disable for this specific line instead of relaxing the rule globally.

Also applies to: 117-117

docs/src/app/(home)/tools/ctr-calculator/page.tsx (1)

120-491: Page composition, content structure, and CTA wiring look solid

Breadcrumbs, CTRCalculatorForm, educational sections, FAQ accordion, RelatedTools, and the CTA are composed cleanly and are consistent with the other tools. Nothing blocking from a correctness or UX perspective here.

docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)

8-231: UTM URL construction and form UX look good

The memoized UTM URL builder, required vs optional fields, conditional result section, and copy‑with‑feedback flow are all straightforward and correct for common UTM use cases.

docs/src/app/(home)/tools/seo-title-generator/SEOTitleForm.tsx (1)

15-199: Fetch flow, rate‑limit handling, and result presentation look solid

The POST flow to /api/tools/generate-seo-title, header-based rate‑limit display, per‑item copy state, and length coloring logic are all consistent and correct. No blocking issues from a logic standpoint.

docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1)

129-547: Overall page structure and educational content look strong

Breadcrumbs, SampleSizeForm integration, well‑layered educational sections, FAQ accordion, RelatedTools, and the CTA are all wired correctly and read well; no functional concerns here.

docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx (1)

60-180: Detection flow, result rendering, and rate‑limit display look good

The POST flow to /api/tools/detect-analytics, handling of X-RateLimit-Remaining, summary + privacy score card, and platform list rendering are all straightforward and functionally sound.

docs/src/app/(home)/tools/analytics-detector/page.tsx (1)

118-355: Page composition and content restructuring look solid

Breadcrumbs, AnalyticsDetectorForm, expanded educational sections, updated FAQ, and RelatedTools integration are wired correctly and give a consistent UX with other tools. No functional issues spotted in this section.

docs/src/app/(home)/tools/utm-builder/page.tsx (2)

8-41: Metadata configuration is consistent and SEO‑friendly

Title/description/keywords plus OpenGraph, Twitter, and canonical URL for https://docs.rybbit.io/tools/utm-builder are coherent and match the route structure. No issues here.


116-427: Form embedding and educational content are cleanly structured

UTMBuilderForm integration, Breadcrumbs/RelatedTools usage, and the surrounding explanatory sections (parameters, naming conventions, use cases, mistakes, how‑to, CTA) are well organized and consistent with other tools. I don’t see functional issues in this block.

docs/src/app/(home)/tools/og-tag-generator/OGTagForm.tsx (2)

26-58: generateOGTags flow and error handling look good

Input validation, loading state management, rate‑limit header parsing, and API error surface (data.error fallback) are all reasonable. No blocking issues here.


66-205: Form UX and results rendering are solid

Field wiring, disabled states during loading, per‑variation copy feedback, and the summary layout for generated OG tag variations are all clear and consistent. No further issues from my side.

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (2)

8-43: Traffic value page metadata is coherent

Metadata (title, description, keywords, OpenGraph/Twitter, canonical) aligns with the route and page content; nothing problematic here.


118-403: Content sections and TrafficValueForm integration look correct

Breadcrumbs, the form block, explanatory sections (what traffic value is, formula, factors, monetization strategies, benchmarks), RelatedTools, and CTA are all wired as expected. I don’t see functional or structural issues in this portion.

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (2)

8-40: Bounce rate metadata setup is consistent and appropriate

Metadata for title/description, keyword set, OpenGraph/Twitter, and canonical URL all match the route and content; no changes needed.


115-377: Form integration and descriptive sections are well implemented

BounceRateForm usage, Breadcrumbs, the explanatory sections (what bounce rate is, why it matters, formula, benchmarks, how to reduce it), RelatedTools, and the CTA all look consistent and functionally sound. No issues to flag here.

docs/src/app/(home)/tools/page-speed-calculator/page.tsx (2)

6-45: Metadata configuration looks correct and aligned with the tool URL

Title, description, keywords, Open Graph, Twitter, and canonical fields are coherent and point to https://rybbit.io/tools/page-speed-calculator. No issues from a Next.js Metadata perspective.


118-209: Page composition with Breadcrumbs, form, and related tools is clean

The structure (Breadcrumbs → hero copy → educational sections → PageSpeedFormRelatedTools) is clear and consistent with other tools; nothing blocking from a correctness or UX standpoint.

docs/src/app/(home)/tools/privacy-policy-builder/page.tsx (2)

117-323: Overall page structure and disclaimers look solid

Breadcrumbs, educational sections, PrivacyPolicyBuilderForm, legal disclaimer, FAQ, and RelatedTools are wired together coherently. Nothing stands out as a functional or UX issue in this layout.


8-38: Incorrect characterization of domain usage across tool pages

The review claims "other tool pages use rybbit.io or docs.rybbit.io," but verification shows tool pages actually use three different domains: rybbit.com (6 tools), docs.rybbit.io (5 tools), and rybbit.io (3 tools). The privacy-policy-builder page using rybbit.com is not an outlier—six other tool pages also use rybbit.com, including analytics-detector, meta-description-generator, og-tag-generator, sample-size-calculator, and seo-title-generator.

While there is a legitimate domain inconsistency issue across tools that warrants addressing, the review's characterization of which domains are in use is inaccurate.

Likely an incorrect or invalid review comment.

docs/src/app/(home)/tools/meta-description-generator/page.tsx (1)

111-304: Form integration and FAQ/education content are wired correctly

MetaDescriptionForm is composed cleanly with the hero, best-practices sections, FAQ, and RelatedTools. No obvious logic, accessibility, or routing issues here.

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1)

124-533: Rich legal/UX content, form, FAQ, and CTA are composed correctly

The PrivacyPolicyForm, disclaimers, compliance sections, FAQ, RelatedTools, and CTA with TrackedButton/DEFAULT_EVENT_LIMIT are structured cleanly and appear consistent with the rest of the tooling pages. No functional issues spotted.

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)

111-505: Form, educational content, FAQ, RelatedTools, and CTA wiring look correct

MarketingROIForm is integrated cleanly with the hero, multi-section educational content, FAQ (aligned with the JSON-LD FAQ entries), RelatedTools, and the final TrackedButton CTA. No correctness or UX concerns from this composition.

docs/src/app/(home)/tools/og-tag-generator/page.tsx (1)

10-41: Verify metadata URLs and alignment with deployed route

The openGraph.url and alternates.canonical values are hard‑coded to https://rybbit.com/tools/og-tag-generator. Please double‑check that this domain and path exactly match the deployed URL for this page so you don’t end up with mismatched canonical/OG URLs if routing or domains change.

docs/src/app/(home)/tools/seo-title-generator/page.tsx (1)

8-37: Confirm metadata URLs and path consistency

As with the OG page, openGraph.url and alternates.canonical are hard‑coded to https://rybbit.com/tools/seo-title-generator. Please confirm this exactly matches the live route (including domain and any subpaths) so canonical and OG signals remain correct if routing changes.

Comment on lines +26 to +35
const detectAnalytics = async () => {
if (!url) {
setError("Please enter a website URL");
return;
}

setIsLoading(true);
setError("");
setResult(null);

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

Clear stale results on validation error and guard against double submits

If the user has a previous detection result and then clicks “Detect Analytics” with an empty URL, you set an error but keep showing the old result. Also, pressing Enter in the input while a request is in flight can fire another detectAnalytics call.

Consider:

const detectAnalytics = async () => {
-  if (!url) {
-    setError("Please enter a website URL");
-    return;
-  }
+  if (!url) {
+    setError("Please enter a website URL");
+    setResult(null);
+    return;
+  }
+
+  if (isLoading) return;

This avoids stale results when validation fails and prevents accidental double submissions via the Enter key.

Also applies to: 80-86

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx around
lines 26-35 and 80-86, validation currently sets an error but leaves any
previous result visible and nothing prevents re-submitting while a request is in
flight. Fix by clearing setResult(null) whenever validation fails (e.g., when
url is empty) so stale results are not shown, and add a guard at the start of
detectAnalytics to return early if isLoading is true to prevent double
submissions (also ensure any Enter-key handler checks isLoading before calling
detectAnalytics).

Comment on lines 43 to 118
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
name: "Open Graph Tag Generator",
applicationCategory: "SEO Tool",
description: "Free tool to generate optimized Open Graph tags for social sharing",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
featureList: [
"AI-powered OG tag generation",
"Multiple tag variations",
"Twitter Card optimization",
"Social preview preview",
"HTML code generation",
"Image size recommendations",
],
operatingSystem: "Any",
url: "https://rybbit.com/tools/og-tag-generator",
},
{
"@type": "FAQPage",
mainEntity: [
{
"@type": "Question",
name: "What are Open Graph tags and why do I need them?",
acceptedAnswer: {
"@type": "Answer",
text: "Open Graph (OG) tags are meta tags in your HTML head that control how your content appears when shared on social media platforms like Facebook, LinkedIn, and Twitter. They define the title, description, image, and type of content that appears in the social preview. Without OG tags, social media platforms use their default parsing which often results in poor-looking previews. Optimized OG tags significantly increase click-through rates and engagement when content is shared.",
},
},
{
"@type": "Question",
name: "What are the required vs optional Open Graph tags?",
acceptedAnswer: {
"@type": "Answer",
text: "Required OG tags are: og:title, og:type, og:image, and og:url. These four tags provide the minimum information needed for a social preview. Optional but recommended tags include og:description (summary of content), og:locale (language), and og:site_name. For articles, add og:article:published_time and og:article:author. Twitter Cards require card type, title, and description at minimum.",
},
},
{
"@type": "Question",
name: "What are the correct image sizes and specifications for OG tags?",
acceptedAnswer: {
"@type": "Answer",
text: "For og:image, use images that are 1200x630 pixels (1.91:1 aspect ratio) for optimal display on most platforms. Minimum recommended size is 200x200 pixels. File size should be under 8MB. Twitter Card images for summary_large_image should be 506x506 minimum, preferably 1024x512. Use JPG or PNG formats. For og:image:width and og:image:height, specify the actual dimensions of your image. Square images work best for profiles and smaller feeds.",
},
},
{
"@type": "Question",
name: "How do I test my Open Graph tags?",
acceptedAnswer: {
"@type": "Answer",
text: "Use Facebook's Sharing Debugger (facebook.com/developers/tools/debug/) to see how your pages appear on Facebook. Use LinkedIn's Post Inspector (linkedin.com/feed/update/urn:li:activity) to test LinkedIn previews. For Twitter, use the Card Validator (cards-dev.twitter.com/validator). Always clear the cache after making changes. Test on actual social platforms by pasting your URL in the status update box. Track social referral traffic with Rybbit Analytics to see which OG tags drive the most clicks.",
},
},
{
"@type": "Question",
name: "What's the difference between Open Graph tags and Twitter Cards?",
acceptedAnswer: {
"@type": "Answer",
text: "Open Graph tags are a protocol developed by Facebook that works across multiple platforms. Twitter Cards use the twitter: prefix and allow more granular control over Twitter-specific previews. Twitter Cards types include summary (small card with summary), summary_large_image (large image card), app (mobile app card), and player (video player card). You should implement both OG tags and Twitter Card tags for complete social optimization. Twitter will fall back to OG tags if Twitter Card tags are missing.",
},
},
],
},
],
};

export default function OGTagGeneratorPage() {
return (
<div className="min-h-screen bg-gradient-to-b from-neutral-50 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<div className="max-w-4xl mx-auto px-6 py-20">
{/* Header */}
<div className="mb-16">
<div className="inline-block mb-4 px-4 py-1.5 bg-emerald-100 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 rounded-full">
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-400">AI-Powered Tool</span>
</div>
<h1 className="text-5xl md:text-6xl font-bold text-neutral-900 dark:text-white mb-6 tracking-tight">
Open Graph Tag Generator
</h1>
<p className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl leading-relaxed">
Generate optimized Open Graph tags for social media sharing. Get perfect previews on Facebook, Twitter, and LinkedIn.
</p>
{remainingRequests !== null && (
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-4">
{remainingRequests} requests remaining this minute
</p>
)}
</div>

{/* Tool */}
<div className="mb-16">
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-neutral-900 dark:text-white mb-2">
Page Title <span className="text-red-500">*</span>
</label>
<input
type="text"
value={pageTitle}
onChange={e => setPageTitle(e.target.value)}
placeholder="e.g., The Ultimate Guide to Web Analytics"
disabled={isLoading}
className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 disabled:opacity-50"
/>
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
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

🧩 Analysis chain

dangerouslySetInnerHTML for JSON‑LD: safe here but breaking linters; add explicit ignore

structuredData is a static object defined in this file, so injecting it via dangerouslySetInnerHTML is effectively safe from XSS. However, ast‑grep and Biome are flagging this as a security error, which can break CI.

Consider explicitly documenting and suppressing the lint on this line so future reviewers see that it’s an intentional, safe JSON‑LD pattern. For example:

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

If you also have a custom ast‑grep rule, you may want to scope it to only flag cases where the source is not a static object literal.


I'll verify this review comment by checking the file, linter configurations, and whether the suggested fix approach is correct.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if the file exists and examine the code around lines 43-118
if [ -f "docs/src/app/(home)/tools/og-tag-generator/page.tsx" ]; then
  echo "=== FILE EXISTS ==="
  sed -n '40,120p' "docs/src/app/(home)/tools/og-tag-generator/page.tsx"
else
  echo "File not found at expected path"
  # Try to find it
  find . -name "*og-tag-generator*" -type f 2>/dev/null | head -10
fi

Length of output: 4504


🏁 Script executed:

#!/bin/bash
# Look for Biome configuration files
echo "=== SEARCHING FOR BIOME CONFIG ==="
find . -name "biome.json" -o -name "biome.jsonc" -o -name ".biomeignore" 2>/dev/null | head -10

# Check if biome.json exists and show its content
if [ -f "biome.json" ]; then
  echo "=== BIOME.JSON CONTENT ==="
  cat biome.json
fi

Length of output: 1849


🏁 Script executed:

#!/bin/bash
# Look for ast-grep configuration
echo "=== SEARCHING FOR AST-GREP CONFIG ==="
find . -name "astgrep.yaml" -o -name ".astgrep.yaml" -o -name "ast-grep.yaml" 2>/dev/null | head -10

if [ -f "astgrep.yaml" ]; then
  echo "=== ASTGREP.YAML CONTENT ==="
  cat astgrep.yaml
fi

if [ -f ".astgrep.yaml" ]; then
  echo "=== .ASTGREP.YAML CONTENT ==="
  cat .astgrep.yaml
fi

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Search for any dangerouslySetInnerHTML usage in the codebase to see patterns
echo "=== DANGEROUSLYSETINNERHTML USAGE PATTERNS ==="
rg -n "dangerouslySetInnerHTML" -A2 -B2 --type=tsx --type=ts 2>/dev/null | head -40

Length of output: 46


Let me verify whether Biome actually has this security rule and if it would flag this pattern.


---

🌐 Web query:

```web
Biome noDangerouslySetInnerHtml security rule React

💡 Result:

Summary: Biome includes a security lint rule lint/security/noDangerouslySetInnerHtml that forbids using React's dangerouslySetInnerHTML (to prevent XSS). It's recommended/enabled by default, has default severity "error", and has no automatic fixer. Configure via biome.json under linter.rules.security. [1][2][3]

Quick config example (biome.json):
{
"linter": {
"rules": {
"security": {
"noDangerouslySetInnerHtml": "error"
}
}
}
}

Related: there's also noDangerouslySetInnerHtmlWithChildren which flags using children together with dangerouslySetInnerHTML. [2] Sources: Biome docs. [1][2][3]


Add biome-ignore comment to suppress security rule for static JSON-LD

The review is accurate. Biome's lint/security/noDangerouslySetInnerHtml rule is enabled by default, and it will flag the dangerouslySetInnerHTML usage even though structuredData is a static object with no XSS risk. Adding the biome-ignore comment is the correct pattern to suppress this false positive:

+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
       <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />

This prevents CI breakage while documenting why the security rule is intentionally disabled at this location.

📝 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
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
name: "Open Graph Tag Generator",
applicationCategory: "SEO Tool",
description: "Free tool to generate optimized Open Graph tags for social sharing",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
featureList: [
"AI-powered OG tag generation",
"Multiple tag variations",
"Twitter Card optimization",
"Social preview preview",
"HTML code generation",
"Image size recommendations",
],
operatingSystem: "Any",
url: "https://rybbit.com/tools/og-tag-generator",
},
{
"@type": "FAQPage",
mainEntity: [
{
"@type": "Question",
name: "What are Open Graph tags and why do I need them?",
acceptedAnswer: {
"@type": "Answer",
text: "Open Graph (OG) tags are meta tags in your HTML head that control how your content appears when shared on social media platforms like Facebook, LinkedIn, and Twitter. They define the title, description, image, and type of content that appears in the social preview. Without OG tags, social media platforms use their default parsing which often results in poor-looking previews. Optimized OG tags significantly increase click-through rates and engagement when content is shared.",
},
},
{
"@type": "Question",
name: "What are the required vs optional Open Graph tags?",
acceptedAnswer: {
"@type": "Answer",
text: "Required OG tags are: og:title, og:type, og:image, and og:url. These four tags provide the minimum information needed for a social preview. Optional but recommended tags include og:description (summary of content), og:locale (language), and og:site_name. For articles, add og:article:published_time and og:article:author. Twitter Cards require card type, title, and description at minimum.",
},
},
{
"@type": "Question",
name: "What are the correct image sizes and specifications for OG tags?",
acceptedAnswer: {
"@type": "Answer",
text: "For og:image, use images that are 1200x630 pixels (1.91:1 aspect ratio) for optimal display on most platforms. Minimum recommended size is 200x200 pixels. File size should be under 8MB. Twitter Card images for summary_large_image should be 506x506 minimum, preferably 1024x512. Use JPG or PNG formats. For og:image:width and og:image:height, specify the actual dimensions of your image. Square images work best for profiles and smaller feeds.",
},
},
{
"@type": "Question",
name: "How do I test my Open Graph tags?",
acceptedAnswer: {
"@type": "Answer",
text: "Use Facebook's Sharing Debugger (facebook.com/developers/tools/debug/) to see how your pages appear on Facebook. Use LinkedIn's Post Inspector (linkedin.com/feed/update/urn:li:activity) to test LinkedIn previews. For Twitter, use the Card Validator (cards-dev.twitter.com/validator). Always clear the cache after making changes. Test on actual social platforms by pasting your URL in the status update box. Track social referral traffic with Rybbit Analytics to see which OG tags drive the most clicks.",
},
},
{
"@type": "Question",
name: "What's the difference between Open Graph tags and Twitter Cards?",
acceptedAnswer: {
"@type": "Answer",
text: "Open Graph tags are a protocol developed by Facebook that works across multiple platforms. Twitter Cards use the twitter: prefix and allow more granular control over Twitter-specific previews. Twitter Cards types include summary (small card with summary), summary_large_image (large image card), app (mobile app card), and player (video player card). You should implement both OG tags and Twitter Card tags for complete social optimization. Twitter will fall back to OG tags if Twitter Card tags are missing.",
},
},
],
},
],
};
export default function OGTagGeneratorPage() {
return (
<div className="min-h-screen bg-gradient-to-b from-neutral-50 to-neutral-100 dark:from-neutral-950 dark:to-neutral-900">
<div className="max-w-4xl mx-auto px-6 py-20">
{/* Header */}
<div className="mb-16">
<div className="inline-block mb-4 px-4 py-1.5 bg-emerald-100 dark:bg-emerald-900/30 border border-emerald-200 dark:border-emerald-800 rounded-full">
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-400">AI-Powered Tool</span>
</div>
<h1 className="text-5xl md:text-6xl font-bold text-neutral-900 dark:text-white mb-6 tracking-tight">
Open Graph Tag Generator
</h1>
<p className="text-xl text-neutral-600 dark:text-neutral-400 max-w-2xl leading-relaxed">
Generate optimized Open Graph tags for social media sharing. Get perfect previews on Facebook, Twitter, and LinkedIn.
</p>
{remainingRequests !== null && (
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-4">
{remainingRequests} requests remaining this minute
</p>
)}
</div>
{/* Tool */}
<div className="mb-16">
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-neutral-900 dark:text-white mb-2">
Page Title <span className="text-red-500">*</span>
</label>
<input
type="text"
value={pageTitle}
onChange={e => setPageTitle(e.target.value)}
placeholder="e.g., The Ultimate Guide to Web Analytics"
disabled={isLoading}
className="w-full px-4 py-3 bg-white dark:bg-neutral-800 border border-neutral-300 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-emerald-500 disabled:opacity-50"
/>
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
export default function OGTagGeneratorPage() {
return (
<>
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
🧰 Tools
🪛 ast-grep (0.40.0)

[warning] 117-117: 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)

🪛 Biome (2.1.2)

[error] 118-118: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/og-tag-generator/page.tsx around lines 43-118, the
use of dangerouslySetInnerHTML for static JSON-LD is flagged by Biome's
lint/security/noDangerouslySetInnerHtml rule; add a biome-ignore comment
immediately above the script tag to suppress that specific rule for this
known-safe static object, mentioning the rule name
(lint/security/noDangerouslySetInnerHtml) and a short note that the JSON-LD is
static to document the intentional suppression.

Comment on lines +56 to +61
// Bounce rate calculation (assume current is 50% baseline)
const currentBounceRate = 50;
const newBounceRate = timeDifference < 0
? Math.max(currentBounceRate + bounceRateIncrease, 0)
: Math.min(currentBounceRate + bounceRateIncrease, 100);

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 | 🔴 Critical

Fix bounce rate calculation logic.

The bounce rate calculation has inverted logic. When timeDifference > 0 (speed improves), the bounce rate should decrease, not increase. Currently, both branches add bounceRateIncrease, which means improving speed incorrectly increases the bounce rate.

Apply this diff to fix the logic:

    // Bounce rate calculation (assume current is 50% baseline)
    const currentBounceRate = 50;
-   const newBounceRate = timeDifference < 0
-     ? Math.max(currentBounceRate + bounceRateIncrease, 0)
-     : Math.min(currentBounceRate + bounceRateIncrease, 100);
+   const newBounceRate = timeDifference > 0
+     ? Math.max(currentBounceRate - bounceRateIncrease, 0)  // Improving: reduce bounce rate
+     : Math.min(currentBounceRate + bounceRateIncrease, 100);  // Degrading: increase bounce rate
📝 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
// Bounce rate calculation (assume current is 50% baseline)
const currentBounceRate = 50;
const newBounceRate = timeDifference < 0
? Math.max(currentBounceRate + bounceRateIncrease, 0)
: Math.min(currentBounceRate + bounceRateIncrease, 100);
// Bounce rate calculation (assume current is 50% baseline)
const currentBounceRate = 50;
const newBounceRate = timeDifference > 0
? Math.max(currentBounceRate - bounceRateIncrease, 0) // Improving: reduce bounce rate
: Math.min(currentBounceRate + bounceRateIncrease, 100); // Degrading: increase bounce rate
🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/page-speed-calculator/PageSpeedForm.tsx around
lines 56 to 61, the bounce rate logic is inverted: both branches add
bounceRateIncrease so improvements (timeDifference > 0) incorrectly raise bounce
rate. Fix by using subtraction on the improvement branch: when timeDifference >
0 compute Math.max(currentBounceRate - bounceRateIncrease, 0), otherwise (speed
worsens or timeDifference < 0) compute Math.min(currentBounceRate +
bounceRateIncrease, 100); keep currentBounceRate constant and clamp results to
[0,100].

Comment on lines +39 to +42
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
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

🧩 Analysis chain

JSON‑LD injection: static and safe, but align with lint rules

You’re using the standard JSON‑LD pattern:

<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />

Given structuredData is a static, local object, this is safe from user‑controlled XSS. To keep CI happy with Biome and ast‑grep:

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is static JSON-LD defined in this file */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

And, if needed, narrow your custom static‑analysis rule so JSON‑LD cases like this are exempt.

Also applies to: 111-113



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine the file at the specified lines
cat "docs/src/app/(home)/tools/seo-title-generator/page.tsx" | head -120 | tail -90

Length of output: 3481


🏁 Script executed:

#!/bin/bash
# Search for structuredData definition in the file
rg -n "structuredData" "docs/src/app/(home)/tools/seo-title-generator/page.tsx" -A5 -B5

Length of output: 647


🏁 Script executed:

#!/bin/bash
# Check for Biome configuration
fd -e json -e js -e toml | xargs rg -l "biome|dangerouslySetInnerHtml" --type=json --type=js --type=toml 2>/dev/null | head -5

Length of output: 10


🏁 Script executed:

#!/bin/bash
# Look for biome config file
fd "biome\." -type f | head -5

Length of output: 170



---

🏁 Script executed:

```shell
#!/bin/bash
# Check biome configuration for security rules related to dangerouslySetInnerHtml
cat "biome.json"

Length of output: 1774


🏁 Script executed:

#!/bin/bash
# Check lines 111-113 specifically to see exact content
sed -n '110,115p' "docs/src/app/(home)/tools/seo-title-generator/page.tsx"

Length of output: 223


🏁 Script executed:

#!/bin/bash
# Search for any other uses of dangerouslySetInnerHTML in the same file
rg -n "dangerouslySetInnerHTML" "docs/src/app/(home)/tools/seo-title-generator/page.tsx" -B2 -A2

Length of output: 188



---

🌐 Web query:

```web
Biome security rule noDangerouslySetInnerHtml lint rule

💡 Result:

Summary

  • Biome lint rule: security/noDangerouslySetInnerHtml — prevents use of React's dangerouslySetInnerHTML (XSS risk). Default severity: error; recommended/enabled by default. [1][2][3]

How to configure

  • biome.json example:
    {
    "linter": {
    "rules": {
    "security": { "noDangerouslySetInnerHtml": "error" }
    }
    }
    }
  • There is also security/noDangerouslySetInnerHtmlWithChildren to disallow using both children and dangerouslySetInnerHTML. [1][2]

Example (invalid)

  • Reported as lint/security/noDangerouslySetInnerHtml because it can expose users to XSS. [1]

Notes

  • Rule maps to the same check as ESLint's react/no-danger. [1]

Sources
[1] Biome rule page: noDangerouslySetInnerHtml.
[2] Biome rules index (security section).
[3] Biome (es) rule page indicating recommended/default.


Correct line reference: JSON-LD injection at line 112 only (not 111-113)

The suggestion is valid: structuredData is static and safe from XSS, so adding a biome-ignore comment for security/noDangerouslySetInnerHtml is appropriate. However, there is only one instance in this file at line 112, not multiple locations. Update the fix to:

{/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is static JSON-LD defined in this file */}
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>

Applied at line 112 only.

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/seo-title-generator/page.tsx at line 112, update
the single JSON-LD script injection to include the biome-ignore lint comment for
security/noDangerouslySetInnerHtml and ensure it is applied only at that
location; specifically, place the comment immediately above the script tag and
keep the script as type="application/ld+json" with dangerouslySetInnerHTML using
JSON.stringify(structuredData), using this exact inline comment: {/*
biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is static
JSON-LD defined in this file */}.

- Updated multiple tool pages to utilize the new ToolCTA component for call-to-action sections, enhancing consistency across the application.
- Changed URLs in metadata from the documentation domain to the main site for improved SEO and user navigation.
- Adjusted Breadcrumbs for better readability and structure in the UI.
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: 1

♻️ Duplicate comments (5)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1)

9-41: Standardize canonical domain for this tool with other pages

Here both metadata.openGraph.url/alternates.canonical and the JSON-LD url fields use https://rybbit.io/tools/funnel-visualizer, while other tools in this PR use https://rybbit.com/.... It’s worth deciding on a single canonical host for all tools and aligning metadata + JSON-LD across pages (and the sitemap) to avoid split SEO/analytics signals.

Also applies to: 48-52

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)

9-37: Align marketing ROI page domain with the chosen canonical host

This page uses https://rybbit.io/tools/marketing-roi-calculator in both metadata and the JSON-LD url, while some other tools in this PR use https://rybbit.com/.... To avoid SEO/analytics fragmentation and conflicting canonicals, pick a single canonical domain for all tools and update metadata + JSON-LD (and sitemap) accordingly.

Also applies to: 46-48

docs/src/app/(home)/tools/analytics-detector/page.tsx (1)

9-40: Confirm rybbit.com is the intended canonical host for this tool

All metadata/JSON-LD URLs here point to https://rybbit.com/tools/analytics-detector, while other tools in this PR use https://rybbit.io/.... Previous reviews already flagged mixed domains across tool pages; it’d be good to confirm which host actually serves /tools/* and then standardize openGraph.url, alternates.canonical, and JSON-LD URLs across all tools to match.

Also applies to: 46-51, 57-61

docs/src/app/(home)/tools/utm-builder/page.tsx (1)

24-40: Unify domains between metadata and JSON-LD author URL

In this file, metadata.openGraph.url, alternates.canonical, and JSON-LD url use https://rybbit.com/tools/utm-builder, but structuredData.author.url points to https://rybbit.io. That split host within a single page can confuse search engines and analytics attribution. Once you’ve decided on the canonical domain for tools, update all of these URLs to use the same host.

Also applies to: 47-50, 57-60

docs/src/app/(home)/tools/og-tag-generator/page.tsx (1)

42-47: Re-apply JSON‑LD dangerouslySetInnerHTML suppression for linters

This is the same static JSON‑LD pattern previously discussed in review: structuredData is a local constant, and the only consumer is the <script> tag, so there’s no XSS risk. Biome and ast‑grep, however, still flag the dangerouslySetInnerHTML usage.

Reiterating the earlier fix (now applied on other tool pages too):

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

This keeps the security rule intact for real risks while documenting this exception.

Also applies to: 117-117

🧹 Nitpick comments (12)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)

4-35: ToolCTA implementation looks good; optional prop typing tweak for flexibility

The CTA logic and TrackedButton wiring look correct. If you ever need inline markup in title or description, you could widen them to ReactNode without breaking existing string usages.

-import { TrackedButton } from "@/components/TrackedButton";
+import { TrackedButton } from "@/components/TrackedButton";
+import type { ReactNode } from "react";
 
 interface ToolCTAProps {
-  title: string;
-  description: string;
+  title: ReactNode;
+  description: ReactNode;
   eventLocation: string;
   buttonText?: string;
 }
docs/src/components/RelatedTools.tsx (1)

4-102: RelatedTools behavior is solid; dedupe the category union type

Filtering/prioritization logic looks correct and matches the intended UX. To avoid the "seo" | "analytics" | "privacy" union drifting in multiple places, you could centralize it in a shared alias.

-interface Tool {
+type ToolCategory = "seo" | "analytics" | "privacy";
+
+interface Tool {
   name: string;
   description: string;
   href: string;
-  category: "seo" | "analytics" | "privacy";
+  category: ToolCategory;
 }
 
 interface RelatedToolsProps {
   currentToolHref: string;
-  category?: "seo" | "analytics" | "privacy";
+  category?: ToolCategory;
   maxTools?: number;
 }
docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1)

45-121: Deduplicate FAQ content between JSON-LD and the Accordion

The FAQ Q&A pairs are manually duplicated in both structuredData.@graph[1].mainEntity and the Accordion JSX, which makes future edits easy to miss. You can centralize them in a single faqItems array and derive both the JSON-LD and the Accordion from that, reusing the same JSON-LD injection pattern (and any lint ignore/helper) as on the Bounce Rate page.

Example sketch:

const faqItems = [
  {
    question: "Do I need a privacy policy for my website?",
    answer:
      "Yes, most websites need a privacy policy. It's legally required if you collect personal data...",
  },
  // ...
];

const structuredData = {
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "WebApplication",
      // ...
    },
    {
      "@type": "FAQPage",
      mainEntity: faqItems.map(item => ({
        "@type": "Question",
        name: item.question,
        acceptedAnswer: { "@type": "Answer", text: item.answer },
      })),
    },
  ],
};

and in JSX:

{faqItems.map((item, index) => (
  <AccordionItem key={index} value={`item-${index + 1}`}>
    <AccordionTrigger>{item.question}</AccordionTrigger>
    <AccordionContent>{item.answer}</AccordionContent>
  </AccordionItem>
))}

This keeps on-page content and JSON-LD in sync while still using a static structuredData object for the dangerouslySetInnerHTML JSON-LD script as discussed for the Bounce Rate page.

Also applies to: 125-126, 427-512

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1)

41-43: JSON-LD injection pattern is safe here but clashes with lint rules

Using dangerouslySetInnerHTML with a compile-time structuredData object is a standard way to emit JSON-LD and is effectively safe as long as no user input flows into it, which is the case here. To satisfy Biome while keeping this pattern, you can add a targeted ignore comment or wrap this in a small <JsonLd> helper that centralizes the dangerouslySetInnerHTML usage.

/* biome-ignore lint/security/noDangerouslySetInnerHtml -- structuredData is static JSON-LD */
<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>

Also applies to: 61-107, 111-113

docs/src/app/(home)/tools/components/index.ts (1)

1-1: ToolCTA barrel export is correct; consider consolidating imports

Re-exporting ToolCTA here is fine and makes the barrel more useful. You might optionally update tool pages to import from "../components" instead of "../components/ToolCTA" for consistency, but the current setup is functionally correct.

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1)

44-112: Hoist schemaData outside the component to avoid per-render allocation

schemaData is static and doesn’t depend on props or state. Moving it to a top-level const (like you do in some other pages) will avoid recreating the object on every render, even if the impact is small for this mostly-static page.

docs/src/app/(home)/tools/utm-builder/page.tsx (1)

42-45: JSON-LD injection via dangerouslySetInnerHTML is static; consider adding a Biome ignore/helper

Here structuredData is a static in-module object and you inject JSON.stringify(structuredData) into a <script type="application/ld+json">. From a security standpoint this is fine (no user input), but Biome flags dangerouslySetInnerHTML with lint/security/noDangerouslySetInnerHtml.

If Biome is gating CI, consider either:

  • Adding a one-line ignore with rationale:
// biome-ignore lint/security/noDangerouslySetInnerHtml: injecting static JSON-LD schema data
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
  • Or wrapping this pattern in a small <JsonLdScript data={...} /> helper that centralizes the ignore, then reusing it in other tool pages that embed JSON-LD.

This same pattern appears on the other tool pages using JSON-LD, so you can apply the same solution there.

Also applies to: 112-113

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1)

44-47: Suppress lints for static JSON‑LD dangerouslySetInnerHTML usage

structuredData is a static object defined in this file, so injecting it as JSON‑LD is effectively safe. The Biome and ast‑grep warnings are still going to fail CI unless you explicitly suppress this one known‑safe use.

I suggest adding a targeted ignore comment and formatting the script on multiple lines:

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

If you also enforce the ast‑grep rule, you may want to narrow it to non‑JSON‑LD cases rather than this pattern.

Also applies to: 115-115

docs/src/app/(home)/tools/ctr-calculator/page.tsx (1)

44-47: Suppress Biome/security lint for static JSON‑LD script

As with the other tool pages, structuredData here is a local, static JSON object, so using dangerouslySetInnerHTML for the JSON‑LD <script> is safe, but Biome is still flagging it as a security error.

You can make this intent explicit and keep CI green with an inline ignore:

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

Consider aligning any ast‑grep rule with this exception as well so JSON‑LD inserts don’t cause noise.

Also applies to: 117-117

docs/src/app/(home)/tools/sample-size-calculator/page.tsx (2)

5-10: Use a type‑only import for Metadata for consistency and clarity

Here Metadata is only used as a type annotation, but it’s imported as a value:

import { Metadata } from "next";

To align with Next.js patterns (and your other tool pages), consider switching to a type‑only import:

-import { Metadata } from "next";
+import type { Metadata } from "next";

This makes the intent explicit and avoids any chance of a runtime named import being preserved by the compiler.


43-47: Add explicit ignore for static JSON‑LD dangerouslySetInnerHTML

structuredData is defined locally as a plain object and never includes user input, so using dangerouslySetInnerHTML to emit JSON‑LD is safe. Biome still treats this as a security error.

You can document the intent and silence the rule with:

-      <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
+      <script
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
+      />

Adjust any ast‑grep rule similarly if it’s part of your CI.

Also applies to: 126-126

docs/src/app/(home)/tools/og-tag-generator/page.tsx (1)

5-10: Prefer import type { Metadata } instead of a value import

Here too Metadata is only used as a type for the metadata export, but it’s imported as a runtime value:

import { Metadata } from "next";

For consistency with other pages and Next.js docs, consider:

-import { Metadata } from "next";
+import type { Metadata } from "next";

This keeps the import tree clean and clearly marks Metadata as type‑only.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68ed61a and e88fa4a.

📒 Files selected for processing (15)
  • docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/analytics-detector/page.tsx (3 hunks)
  • docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/components/ToolCTA.tsx (1 hunks)
  • docs/src/app/(home)/tools/components/index.ts (1 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/og-tag-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/page.tsx (1 hunks)
  • docs/src/app/\(home\)/tools/ai-privacy-policy-generator/page.tsx (1 hunks)
  • docs/src/app/blog/[...slug]/page.tsx (2 hunks)
  • docs/src/components/RelatedTools.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
docs/src/app/(home)/tools/sample-size-calculator/page.tsx (7)
docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)
  • metadata (9-37)
docs/src/app/(home)/oss-friends/page.tsx (1)
  • metadata (105-108)
docs/src/app/(home)/tools/seo-title-generator/page.tsx (1)
  • metadata (8-37)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/sample-size-calculator/SampleSizeForm.tsx (1)
  • SampleSizeForm (5-178)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-152)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
docs/src/components/TrackedButton.tsx (1)
  • TrackedButton (16-31)
docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/bounce-rate-calculator/BounceRateForm.tsx (1)
  • BounceRateForm (17-233)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/marketing-roi-calculator/MarketingROIForm.tsx (1)
  • MarketingROIForm (12-211)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/og-tag-generator/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/og-tag-generator/OGTagForm.tsx (1)
  • OGTagForm (16-205)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/utm-builder/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)
  • UTMBuilderForm (8-233)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/ctr-calculator/page.tsx (4)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx (1)
  • CTRCalculatorForm (15-158)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-152)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
🪛 ast-grep (0.40.0)
docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[warning] 125-125: 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)

docs/src/app/\(home\)/tools/ai-privacy-policy-generator/page.tsx

[warning] 124-124: 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)

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx

[warning] 111-111: 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)

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx

[warning] 111-111: 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)

docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[warning] 115-115: 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)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[warning] 116-116: 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)

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx

[warning] 125-125: 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)

docs/src/app/(home)/tools/utm-builder/page.tsx

[warning] 112-112: 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)

docs/src/app/(home)/tools/analytics-detector/page.tsx

[warning] 113-113: 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)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[warning] 114-114: 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)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[warning] 116-116: 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)

🪛 Biome (2.1.2)
docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[error] 126-126: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/\(home\)/tools/ai-privacy-policy-generator/page.tsx

[error] 125-125: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx

[error] 112-112: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx

[error] 112-112: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[error] 116-116: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[error] 117-117: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx

[error] 126-126: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/utm-builder/page.tsx

[error] 113-113: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/analytics-detector/page.tsx

[error] 114-114: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[error] 115-115: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[error] 117-117: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
🔇 Additional comments (10)
docs/src/app/blog/[...slug]/page.tsx (2)

171-174: LGTM - Formatting improvement.

The Pre component props are now spread across multiple lines for better readability. No functional changes.


22-22: Domain migration change is consistent across the codebase—no issues detected in code.

Verification shows:

  • No references to docs.rybbit.io exist anywhere in the codebase
  • https://rybbit.com is consistently used throughout the documentation site (layout metadata, robots.ts, sitemap.ts, blog pages, and all tool pages)
  • Line 22 change aligns with the established domain strategy

The code change is correct and consistent. However, redirect configuration from the old domain (if any) cannot be verified through code inspection alone.

docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (1)

7-527: AI Privacy Policy Generator page wiring and content look consistent

Metadata, JSON-LD, the PrivacyPolicyForm, explanatory sections, FAQ, and the bottom signup CTA are all coherently wired; I don’t see functional or SEO issues in this version.

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1)

114-369: Bounce Rate Calculator layout and props are correctly wired

Breadcrumbs items, BounceRateForm usage, explanatory sections, RelatedTools (/tools/bounce-rate-calculator, category="analytics"), and the ToolCTA all look consistent and correct; I don’t see issues with slugs, URLs, or copy here.

docs/src/app/\(home\)/tools/ai-privacy-policy-generator/page.tsx (6)

1-5: AI summary mentions missing components.

The AI summary states that Breadcrumbs, RelatedTools, and ToolCTA components are integrated and imported, but these are not present in the actual code. The component directly implements the CTA section (lines 509-527) instead of using a ToolCTA component, and breadcrumbs/related tools are absent.


7-42: Comprehensive metadata configuration.

The metadata export is well-structured with detailed SEO optimization including title, description, keywords, OpenGraph, Twitter cards, and canonical URL. This will significantly improve search engine visibility and social media sharing.


125-125: Safe usage of dangerouslySetInnerHTML for JSON-LD.

The static analysis warning can be safely ignored. This usage injects JSON-LD structured data from a compile-time constant (structuredData) defined in the same file. Since JSON.stringify() produces JSON (not executable code) and no user input is involved, there is no XSS risk. This is the standard pattern for embedding schema.org structured data.


122-507: Well-structured and comprehensive content.

The component is logically organized with clear sections covering essential privacy policy topics (GDPR, CCPA, required sections, legal considerations). The FAQ implementation using Accordion components provides good UX, and the disclaimer (lines 147-153) appropriately sets expectations about legal review requirements.


508-527: Effective CTA with proper event tracking.

The CTA section effectively promotes Rybbit's analytics while maintaining thematic consistency with the privacy-focused tool. The TrackedButton implementation correctly includes event name and location properties for conversion tracking.


44-120: This review comment is incorrect. The domain split between rybbit.io and rybbit.com is intentional, not an inconsistency.

From the codebase search results, this pattern is consistent across all tool pages: the Organization author URL in schema.org uses https://rybbit.io (the corporate/brand domain), while the tool pages, canonical URLs, and marketing content use https://rybbit.com (the documentation/marketing domain). This is a deliberate multi-domain strategy reflected throughout the codebase, including app.rybbit.io for the SaaS application and rybbit.com for marketing and documentation. The code change does not introduce any inconsistency.

Likely an incorrect or invalid review comment.

- Deleted the AI Privacy Policy Generator tool page and its associated components, including the PrivacyPolicyForm and related metadata.
- Updated the tools list to reflect the removal of the AI Privacy Policy Generator, enhancing clarity and focus on remaining tools.
- Adjusted educational content across various tools for improved readability and consistency.
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: 5

♻️ Duplicate comments (3)
docs/src/app/(home)/tools/og-tag-generator/page.tsx (1)

117-117: Previous security lint suppression suggestion not yet applied.

The biome-ignore comment to suppress the lint/security/noDangerouslySetInnerHtml warning was suggested in a previous review but hasn't been added yet. This will continue to cause static analysis tool failures.

Apply this diff to add the suppression comment:

+      {/* biome-ignore lint/security/noDangerouslySetInnerHtml: structuredData is a static, local JSON-LD object */}
       <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} />
docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (2)

88-88: Fix mathematical error in FAQ structured data (still present from previous review)

The FAQ states "$2.81 per visitor" but the correct calculation is:

  • 50,000 × 2.5% × $75 × 30% = $28,125 total profit
  • $28,125 ÷ 50,000 = $0.56 per visitor

This error contradicts the correct step-by-step calculation shown at line 170 and was flagged in the previous review but remains unfixed.

Apply this diff to correct the FAQ structured data:

-            text: "Traffic value is calculated as: Monthly Visitors × Conversion Rate × Average Order Value × Profit Margin. For example, 50,000 visitors × 2.5% conversion × $75 AOV × 30% margin = $2.81 per visitor in monthly profit.",
+            text: "Traffic value is calculated as: Monthly Visitors × Conversion Rate × Average Order Value × Profit Margin. For example, 50,000 visitors × 2.5% conversion × $75 AOV × 30% margin = approximately $0.56 per visitor in monthly profit.",

153-156: Fix mathematical errors in narrative example (still present from previous review)

The example contains two mathematical errors that contradict the correct step-by-step calculation at line 170:

  1. "$2.81 per visitor" should be "$0.56 per visitor"
    • Calculation: $28,125 profit ÷ 50,000 visitors = $0.56
  2. "nearly $1,400" should be "approximately $2,800"
    • 10% increase: 55,000 visitors generates $30,937.50 profit
    • Increase: $30,937.50 - $28,125 = $2,812.50

These errors were flagged in the previous review but remain unfixed.

Apply this diff to correct the narrative:

-              For example, if you have 50,000 monthly visitors, a 2.5% conversion rate, an average order value of $75,
-              and a 30% profit margin, your traffic value would be approximately $2.81 per visitor in monthly profit.
-              This means increasing your traffic by just 10% could add nearly $1,400 to your monthly profit.
+              For example, if you have 50,000 monthly visitors, a 2.5% conversion rate, an average order value of $75,
+              and a 30% profit margin, your traffic value would be approximately $0.56 per visitor in monthly profit.
+              This means increasing your traffic by just 10% could add approximately $2,800 to your monthly profit.
🧹 Nitpick comments (3)
docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1)

68-120: Consider centralizing FAQ content to drive both JSON-LD and the Accordion

You currently duplicate the six FAQ Q&As in both structuredData.FAQPage.mainEntity and the Accordion items. This is fine functionally, but it’s easy for them to drift out of sync over time.

You could define a single const faqItems = [...] data structure (id, question, answer) and:

  • Map it into mainEntity for JSON-LD.
  • Map it into <AccordionItem>s for the UI.

That would keep content consistent and make future edits cheaper.

Also applies to: 315-399

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (2)

44-112: Consider moving static schema data outside the component.

The JSON-LD structured data is well-formed and follows schema.org guidelines correctly. However, since schemaData is static and doesn't depend on props or state, defining it inside the component causes it to be recreated on every render.

Move the schema data outside the component function for better performance:

+const schemaData = {
+  "@context": "https://schema.org",
+  "@graph": [
+    // ... (entire schemaData object)
+  ],
+};
+
 export default function FunnelVisualizerPage() {
-  const schemaData = {
-    "@context": "https://schema.org",
-    "@graph": [
-      // ...
-    ],
-  };

116-116: Consider using next/script for JSON-LD injection.

The dangerouslySetInnerHTML usage here is safe since schemaData is a local object without user input. However, Next.js provides a more idiomatic approach using the Script component.

Replace the script tag with Next.js Script component:

+import Script from "next/script";
-<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }} />
+<Script
+  id="schema-funnel-visualizer"
+  type="application/ld+json"
+  dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
+/>

This approach provides better loading optimization and follows Next.js best practices while maintaining the same functionality.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e88fa4a and c5bcd79.

📒 Files selected for processing (9)
  • docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx (0 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/page.tsx (2 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/og-tag-generator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/page-speed-calculator/page.tsx (2 hunks)
  • docs/src/app/(home)/tools/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/sample-size-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/page.tsx (1 hunks)
💤 Files with no reviewable changes (1)
  • docs/src/app/(home)/tools/ai-privacy-policy-generator/page.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx (5)
docs/src/app/(home)/tools/page.tsx (1)
  • metadata (18-22)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (1)
  • FunnelVisualizerForm (20-230)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-152)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/utm-builder/page.tsx (3)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)
  • UTMBuilderForm (8-233)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
docs/src/app/(home)/tools/page-speed-calculator/page.tsx (5)
docs/src/app/(home)/tools/page.tsx (1)
  • metadata (18-22)
docs/src/app/(home)/tools/seo-title-generator/page.tsx (1)
  • metadata (8-37)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/page-speed-calculator/PageSpeedForm.tsx (1)
  • PageSpeedForm (22-365)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-152)
docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (4)
docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)
  • metadata (9-37)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/app/(home)/tools/traffic-value-calculator/TrafficValueForm.tsx (1)
  • TrafficValueForm (7-286)
docs/src/app/(home)/tools/components/ToolCTA.tsx (1)
  • ToolCTA (11-38)
🪛 ast-grep (0.40.0)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[warning] 115-115: 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)

docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[warning] 125-125: 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)

docs/src/app/(home)/tools/utm-builder/page.tsx

[warning] 112-112: 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)

docs/src/app/(home)/tools/page-speed-calculator/page.tsx

[warning] 118-118: 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)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[warning] 116-116: 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)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[warning] 116-116: 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)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[warning] 114-114: 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)

🪛 Biome (2.1.2)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx

[error] 116-116: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/sample-size-calculator/page.tsx

[error] 126-126: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/utm-builder/page.tsx

[error] 113-113: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/page-speed-calculator/page.tsx

[error] 119-119: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/og-tag-generator/page.tsx

[error] 117-117: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/ctr-calculator/page.tsx

[error] 117-117: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx

[error] 115-115: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)
🔇 Additional comments (23)
docs/src/app/(home)/tools/sample-size-calculator/page.tsx (2)

9-41: SEO metadata and hero/tool framing look solid

The metadata block is comprehensive (title, description, keywords, OG, Twitter, canonical) and matches the visible hero copy and tool purpose. This should play nicely with search + social previews without any obvious inconsistencies.

Also applies to: 138-153


43-121: JSON-LD via dangerouslySetInnerHTML is acceptable here but should remain strictly static

Using a <script type="application/ld+json"> with dangerouslySetInnerHTML is a standard pattern for JSON-LD, and in this case the structuredData object is fully hard-coded in this file, so there’s no realistic XSS surface right now.

The main thing to watch long-term is that nothing user-controlled or request-scoped ever gets interpolated into structuredData. If that ever happens, it should be sanitized or encoded before stringification, or built on the server and injected via a safer channel.

If your lint rules allow it, you may want to add a local disable + comment clarifying that this is intentional, schema-only usage so future changes don’t accidentally weaken the safety invariants.

Also applies to: 123-127

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (2)

1-7: LGTM!

All imports are properly utilized throughout the component and follow Next.js conventions.


114-348: LGTM! Well-structured content-rich page.

The page structure effectively combines:

  • Clear navigation with Breadcrumbs
  • Informative header section
  • Interactive tool integration via FunnelVisualizerForm
  • Comprehensive educational content
  • SEO-friendly FAQ section that mirrors the JSON-LD schema
  • Related tools discovery
  • Strong CTA using ToolCTA component

The modular composition using shared components (Breadcrumbs, RelatedTools, ToolCTA) promotes maintainability and consistency across tool pages.

docs/src/app/(home)/tools/traffic-value-calculator/page.tsx (4)

1-5: LGTM!

The imports are well-organized and align with the component-based refactoring approach used across other tool pages.


115-115: Static analysis false positive: dangerouslySetInnerHTML is safe here

The static analysis warning about dangerouslySetInnerHTML can be safely ignored in this context. The structured data is hardcoded and contains no user input, making this a standard and secure pattern for injecting JSON-LD schema markup in Next.js applications.


144-272: Excellent educational content structure

The comprehensive educational sections (What is Traffic Value, How to Calculate, Factors Affecting, Strategies to Maximize, Industry Benchmarks) provide substantial value to users. The step-by-step calculation at line 170 correctly demonstrates the $0.56 per visitor result, making it a reliable reference once the inconsistencies in earlier sections are corrected.


117-283: Well-structured component refactoring

The page successfully follows the content-rich educational pattern seen in other tool pages, with:

  • Clean separation of concerns (form logic in TrafficValueForm)
  • Breadcrumbs for navigation
  • Comprehensive educational content
  • Related tools and CTA sections

This aligns well with the PR objectives to enhance tool pages for better SEO and discoverability.

docs/src/app/(home)/tools/utm-builder/page.tsx (7)

1-5: LGTM!

The imports are correctly structured and align with Next.js 15 conventions. All imported components are properly referenced in the code.


24-40: LGTM!

The OpenGraph, Twitter card, and canonical URL metadata are properly structured and follow Next.js 15 conventions. Good use of SEO best practices.


42-108: Verify URL consistency: rybbit.io vs rybbit.com.

The structured data looks well-formed and follows schema.org standards. However, note that line 59 uses https://rybbit.io for the organization URL, while the rest of the metadata uses rybbit.com. Ensure this is intentional (e.g., if the company site is on .io while the product is on .com).


113-113: Static analysis false positive: JSON-LD injection is safe.

The static analysis tools flagged dangerouslySetInnerHTML as a potential XSS risk. However, this is a false positive because:

  • The content is static and hardcoded (structuredData object defined in this file)
  • No user input is involved
  • JSON.stringify ensures proper encoding
  • This is the standard method for adding JSON-LD structured data in React/Next.js

No action needed.


115-136: LGTM!

The page structure, breadcrumbs, header, and form component integration are well-organized. The responsive layout and typography hierarchy follow good practices.


138-281: LGTM!

The educational content is comprehensive, well-structured, and follows proper semantic HTML practices. The sections cover all essential aspects of UTM parameters with practical examples and best practices. Dark mode styling is properly implemented throughout.


282-290: LGTM!

The RelatedTools and ToolCTA components are properly integrated with appropriate props. The category classification and event tracking location are well-chosen.

docs/src/app/(home)/tools/page.tsx (1)

133-133: LGTM: Styling simplification is consistent with PR goals.

The removal of gradient background classes aligns with the PR's objective to simplify styling across tool pages.

docs/src/app/(home)/tools/page-speed-calculator/page.tsx (4)

6-45: LGTM: Comprehensive SEO metadata.

The metadata export follows Next.js 15 best practices with proper title, description, keywords, OpenGraph, Twitter cards, and canonical URL.


48-119: LGTM: Safe usage of dangerouslySetInnerHTML for structured data.

The dangerouslySetInnerHTML usage is safe here because the schema object is a static constant defined in the component, not user-generated content. This is the standard pattern for injecting JSON-LD structured data for SEO purposes. The structured data correctly follows schema.org standards for WebApplication and FAQPage types.


136-182: LGTM: Valuable educational content enhances page quality.

The educational sections provide context and actionable guidance for users, transforming the page from a simple calculator into a comprehensive resource. Content is well-structured and relevant.


185-186: LGTM: Component-based architecture improves maintainability.

The extraction of calculation logic into PageSpeedForm and addition of RelatedTools follows good separation of concerns and enhances discoverability.

docs/src/app/(home)/tools/ctr-calculator/page.tsx (3)

117-117: LGTM: Safe usage of dangerouslySetInnerHTML for structured data.

The dangerouslySetInnerHTML usage is safe because structuredData is a static constant, not user-generated content. This is the standard pattern for injecting JSON-LD structured data for SEO.


141-294: LGTM: Comprehensive educational content adds significant value.

The extensive educational sections covering CTR concepts, benchmarks, and optimization techniques transform this from a simple calculator into a valuable marketing resource. Content is well-organized and actionable.


121-123: LGTM: Component-based refactor improves architecture.

The use of Breadcrumbs, CTRCalculatorForm, RelatedTools, and ToolCTA components follows good separation of concerns and aligns with the PR's architectural improvements across tool pages.

Also applies to: 139-139, 363-363, 366-370

Comment on lines 9 to 42
export const metadata: Metadata = {
title: "Free CTR Calculator | Click-Through Rate Calculator for Marketing",
description:
"Calculate your click-through rate (CTR) and compare it to industry benchmarks. Free CTR calculator for email, PPC, organic search, and social media campaigns. Learn CTR formulas, industry standards, and how to improve your CTR.",
keywords: [
"CTR calculator",
"click-through rate calculator",
"ctr formula",
"click through rate",
"marketing metrics",
"industry benchmarks",
"CTR improvement",
"ad performance",
"conversion metrics",
"marketing analytics",
],
openGraph: {
title: "Free CTR Calculator | Click-Through Rate Calculator for Marketing",
description:
"Calculate your CTR and see how your campaigns compare to industry benchmarks across email, PPC, organic search, and social media.",
type: "website",
url: "https://rybbit.com/tools/ctr-calculator",
siteName: "Rybbit Documentation",
},
twitter: {
card: "summary_large_image",
title: "Free CTR Calculator | Click-Through Rate Calculator for Marketing",
description:
"Calculate your CTR and compare to industry benchmarks. Understand what's a good click-through rate for your channel.",
},
alternates: {
canonical: "https://rybbit.com/tools/ctr-calculator",
},
};
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

🧩 Analysis chain

Verify domain consistency across tool pages.

The metadata uses rybbit.com domain (lines 30, 40), while the page-speed-calculator tool uses rybbit.io consistently. This domain inconsistency should be resolved to ensure proper SEO and canonical URL handling.


🏁 Script executed:

#!/bin/bash
# Description: Check domain usage consistency across tool pages metadata

# Search for domain usage in metadata exports
rg -n "rybbit\.(com|io)" --type=ts -g "**/tools/**/page.tsx" -C 2

Length of output: 28492


Update domain consistency across all tool metadata.

Verification confirms domain inconsistency affects multiple files, not just the CTR calculator:

  • CTR calculator (lines 30, 40, 49, 52): uses rybbit.com for tool URLs but rybbit.io for Organization (line 62)
  • Other files similarly affected: bounce-rate-calculator, traffic-value-calculator, and others mix rybbit.com and rybbit.io within the same metadata object
  • Pattern: Some tools consistently use rybbit.io (page-speed-calculator, marketing-roi-calculator, funnel-visualizer), while others use rybbit.com (privacy-policy-builder, og-tag-generator, meta-description-generator, seo-title-generator, sample-size-calculator, utm-builder, analytics-detector)

Establish a single domain convention across all tool pages for canonical URLs, openGraph URLs, and schema URLs to maintain SEO integrity and prevent duplicate content issues.

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/ctr-calculator/page.tsx around lines 9 to 42, the
metadata mixes rybbit.com and rybbit.io; update the metadata URLs
(openGraph.url, alternates.canonical, and any other URL fields) to use the
agreed canonical domain rybbit.io so they match the organization info elsewhere,
and then search other tool pages and replace any remaining rybbit.com
occurrences with rybbit.io to maintain a single domain convention across all
tool metadata.

Comment on lines 44 to 112
const structuredData = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
"@id": "https://rybbit.com/tools/ctr-calculator#webapp",
name: "CTR Calculator",
description: "Free tool to calculate click-through rate and compare with industry benchmarks",
url: "https://rybbit.com/tools/ctr-calculator",
applicationCategory: "Utility",
offers: {
"@type": "Offer",
price: "0",
priceCurrency: "USD",
},
author: {
"@type": "Organization",
name: "Rybbit",
url: "https://rybbit.io",
},
},
{
"@type": "FAQPage",
"@id": "https://rybbit.com/tools/ctr-calculator#faqpage",
mainEntity: [
{
"@type": "Question",
name: "What is CTR (Click-Through Rate)?",
acceptedAnswer: {
"@type": "Answer",
text: "CTR is the percentage of people who click on your ad or link after seeing it. It's calculated by dividing the number of clicks by the number of impressions and multiplying by 100. For example, if your ad was shown 10,000 times and received 300 clicks, your CTR is 3%.",
},
},
{
"@type": "Question",
name: "What is a good CTR by channel?",
acceptedAnswer: {
"@type": "Answer",
text: "Average CTRs vary significantly by channel. Search ads average 3.17%, email marketing 2.6%, e-commerce 2.69%, B2B 2.41%, social media 0.90%, and display ads 0.46%. Your target CTR should be benchmarked against your specific channel and industry.",
},
},
{
"@type": "Question",
name: "How do I calculate CTR?",
acceptedAnswer: {
"@type": "Answer",
text: "To calculate CTR, use this formula: CTR = (Clicks / Impressions) × 100. For example, if your ad received 500 clicks from 50,000 impressions, your CTR would be (500 / 50,000) × 100 = 1%. Use our CTR calculator above to compute this automatically.",
},
},
{
"@type": "Question",
name: "How can I improve my CTR?",
acceptedAnswer: {
"@type": "Answer",
text: "To improve CTR: write compelling ad copy with clear value propositions, use strong calls-to-action, improve audience targeting, test different headlines and creatives, ensure ad relevance to search intent, add ad extensions, and improve landing page relevance. Monitor results to identify what works best.",
},
},
{
"@type": "Question",
name: "Why is my CTR different from competitor benchmarks?",
acceptedAnswer: {
"@type": "Answer",
text: "CTR varies based on industry, product type, audience quality, ad positioning, seasonal factors, and competition. Your CTR may legitimately differ from benchmarks due to your unique market position, business model, or targeting strategy. Focus on improving your own CTR over time rather than exactly matching benchmarks.",
},
},
],
},
],
};
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

Domain inconsistency also present in structured data.

The structured data contains the same domain inconsistency: tool URLs use rybbit.com (lines 49, 52, 67) while the author organization URL uses rybbit.io (line 62). This should be aligned with the decision made for the metadata URLs.

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/ctr-calculator/page.tsx around lines 44 to 112
there is a domain mismatch in the structuredData: the tool @id and url entries
use "rybbit.com" while the author organization url uses "rybbit.io"; pick the
canonical metadata domain used across the project (whichever was chosen in
metadata) and update all occurrences in this structuredData block so the @id and
url values use that same domain (make the tool URLs match the chosen domain and
keep all absolute URLs consistent).

Comment on lines 9 to 41
export const metadata: Metadata = {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Input visitor counts at each stage to identify where you're losing customers. Analyze drop-off rates and optimize your funnel for better conversions.",
keywords: [
"funnel visualizer",
"conversion funnel",
"funnel analysis",
"conversion rate optimization",
"sales funnel",
"funnel metrics",
"drop-off analysis",
"customer journey",
"conversion tracking",
"funnel optimization",
],
openGraph: {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
type: "website",
url: "https://rybbit.io/tools/funnel-visualizer",
},
twitter: {
card: "summary_large_image",
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
},
alternates: {
canonical: "https://rybbit.io/tools/funnel-visualizer",
},
};
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

Add images to social media metadata.

The metadata is comprehensive, but both openGraph and twitter configurations are missing image properties. Twitter card type summary_large_image specifically requires an image, and OpenGraph images significantly improve social sharing appearance.

Add image properties to enhance social media previews:

  openGraph: {
    title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
    description:
      "Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
    type: "website",
    url: "https://rybbit.io/tools/funnel-visualizer",
+   images: [{
+     url: "https://rybbit.io/og-images/funnel-visualizer.png",
+     width: 1200,
+     height: 630,
+     alt: "Funnel Visualizer Tool"
+   }],
  },
  twitter: {
    card: "summary_large_image",
    title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
    description:
      "Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
+   images: ["https://rybbit.io/og-images/funnel-visualizer.png"],
  },
📝 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
export const metadata: Metadata = {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Input visitor counts at each stage to identify where you're losing customers. Analyze drop-off rates and optimize your funnel for better conversions.",
keywords: [
"funnel visualizer",
"conversion funnel",
"funnel analysis",
"conversion rate optimization",
"sales funnel",
"funnel metrics",
"drop-off analysis",
"customer journey",
"conversion tracking",
"funnel optimization",
],
openGraph: {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
type: "website",
url: "https://rybbit.io/tools/funnel-visualizer",
},
twitter: {
card: "summary_large_image",
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
},
alternates: {
canonical: "https://rybbit.io/tools/funnel-visualizer",
},
};
export const metadata: Metadata = {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Input visitor counts at each stage to identify where you're losing customers. Analyze drop-off rates and optimize your funnel for better conversions.",
keywords: [
"funnel visualizer",
"conversion funnel",
"funnel analysis",
"conversion rate optimization",
"sales funnel",
"funnel metrics",
"drop-off analysis",
"customer journey",
"conversion tracking",
"funnel optimization",
],
openGraph: {
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
type: "website",
url: "https://rybbit.io/tools/funnel-visualizer",
images: [{
url: "https://rybbit.io/og-images/funnel-visualizer.png",
width: 1200,
height: 630,
alt: "Funnel Visualizer Tool"
}],
},
twitter: {
card: "summary_large_image",
title: "Free Funnel Visualizer | Marketing Funnel Analysis & Conversion Funnel Tool",
description:
"Visualize your conversion funnel step-by-step. Identify drop-off points and optimize conversion rates.",
images: ["https://rybbit.io/og-images/funnel-visualizer.png"],
},
alternates: {
canonical: "https://rybbit.io/tools/funnel-visualizer",
},
};
🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/funnel-visualizer/page.tsx around lines 9 to 41,
the openGraph and twitter metadata omit image properties which are required for
good social previews (and for summary_large_image on Twitter); add an absolute
URL image (or array) to openGraph.images including optional width, height and
alt text, and add the matching image(s) to twitter.images (or twitter.image)
using the same absolute URL; ensure the image path is reachable (https) and
update both objects accordingly so social cards render with a large preview.

description:
"Calculate the monetary value of your website traffic. Understand what each visitor is worth to your business and optimize your marketing spend.",
type: "website",
url: "https://rybbit.com/tools/traffic-value-calculator",
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

🧩 Analysis chain

Verify domain consistency: rybbit.com vs rybbit.io

The URLs in the metadata use rybbit.com, but the structured data at line 61 uses rybbit.io, and the marketing-roi-calculator page also uses rybbit.io. Ensure the domain is consistent across all tool pages for proper SEO and canonical URL handling.

Run the following script to check domain usage across tool pages:

Also applies to: 40-40


🏁 Script executed:

#!/bin/bash
# Check domain usage in tool pages
echo "=== Checking rybbit.com usage ==="
rg -n "rybbit\.com" docs/src/app/\(home\)/tools/

echo ""
echo "=== Checking rybbit.io usage ==="
rg -n "rybbit\.io" docs/src/app/\(home\)/tools/

Length of output: 8739


Fix domain inconsistency in tool metadata and structured data

The traffic-value-calculator and several other tool pages have inconsistent domains: metadata uses rybbit.com while structured data uses rybbit.io. This occurs in:

  • traffic-value-calculator (line 30/40 vs 61)
  • utm-builder (line 29/38/49 vs 59)
  • bounce-rate-calculator, ctr-calculator, and analytics-detector

Standardize to a single domain across metadata, canonical URLs, and structured data within each tool page for consistent SEO handling.

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/traffic-value-calculator/page.tsx around lines 30
and 61, the metadata URL uses "rybbit.com" while the structured data/canonical
uses "rybbit.io"; standardize both to the same domain (choose one, e.g.,
rybbit.io) by replacing the "rybbit.com" value in the metadata/canonical fields
with "rybbit.io" so metadata, canonical URL, and structured data all match.

…e tools directory

- Updated the sitemap function to read tool slugs dynamically from the tools directory, enhancing maintainability and scalability.
- Removed the hardcoded list of tool slugs, streamlining the code and reducing the need for manual updates when adding or removing tools.
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: 1

♻️ Duplicate comments (1)
docs/src/app/sitemap.ts (1)

32-37: Still using new Date() for lastModified.

As noted in the previous review, setting lastModified to the current date on every sitemap generation tells search engines that all tool pages were just modified, even when their content hasn't changed. This dilutes the SEO signal.

Please refer to the previous review comment for suggested solutions using actual file modification times or a stable date.

🧹 Nitpick comments (1)
docs/src/app/sitemap.ts (1)

4-5: Consider using async file system operations for consistency.

The function is async, but uses synchronous readdirSync. While this works in Next.js build contexts, using async operations would be more consistent with the function signature.

Apply this diff to use async operations:

-import { readdirSync } from "fs";
+import { readdir } from "fs/promises";
 import { join } from "path";

Then update the tool discovery:

   const toolsPath = join(process.cwd(), "src/app/(home)/tools");
-  const toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
-    .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
-    .map(dirent => dirent.name);
+  let toolSlugs: string[] = [];
+  try {
+    const dirents = await readdir(toolsPath, { withFileTypes: true });
+    toolSlugs = dirents
+      .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
+      .map(dirent => dirent.name);
+  } catch (error) {
+    console.warn("Could not read tools directory:", error);
+  }

Also applies to: 27-30

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5bcd79 and 2abbebb.

📒 Files selected for processing (1)
  • docs/src/app/sitemap.ts (4 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)

Comment on lines +27 to +30
const toolsPath = join(process.cwd(), "src/app/(home)/tools");
const toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name !== "components")
.map(dirent => dirent.name);
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

Add error handling for directory operations.

readdirSync will throw an exception if the directory doesn't exist or isn't readable, causing the entire sitemap generation to fail. This could break deployments or builds.

Apply this diff to add error handling:

-  const toolsPath = join(process.cwd(), "src/app/(home)/tools");
-  const toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
-    .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
-    .map(dirent => dirent.name);
+  const toolsPath = join(process.cwd(), "src/app/(home)/tools");
+  let toolSlugs: string[] = [];
+  try {
+    toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
+      .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
+      .map(dirent => dirent.name);
+  } catch (error) {
+    console.warn("Could not read tools directory:", error);
+  }
📝 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
const toolsPath = join(process.cwd(), "src/app/(home)/tools");
const toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name !== "components")
.map(dirent => dirent.name);
const toolsPath = join(process.cwd(), "src/app/(home)/tools");
let toolSlugs: string[] = [];
try {
toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
.filter(dirent => dirent.isDirectory() && dirent.name !== "components")
.map(dirent => dirent.name);
} catch (error) {
console.warn("Could not read tools directory:", error);
}
🤖 Prompt for AI Agents
In docs/src/app/sitemap.ts around lines 27 to 30, the call to readdirSync can
throw if the tools directory is missing or unreadable; wrap the directory
operations in a try/catch (or check existence with fs.existsSync/statSync before
calling readdirSync) and fall back to an empty toolSlugs array when the
directory is absent or unreadable, and log a concise warning/error; ensure any
new fs helper used is imported at the top of the file.

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: 0

🧹 Nitpick comments (1)
docs/CLAUDE.md (1)

80-81: Static analysis note: optional URL formatting.

Markdownlint flags a bare URL concern on line 81, though no bare URL is visually apparent in the current text. If the linter continues to flag this, consider wrapping the path patterns in backticks (e.g., `/docs/:path*.mdx`) to clarify they are code patterns rather than URLs.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2abbebb and 77b1bae.

📒 Files selected for processing (3)
  • .gitignore (1 hunks)
  • CLAUDE.md (1 hunks)
  • docs/CLAUDE.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
docs/CLAUDE.md

81-81: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
🔇 Additional comments (2)
.gitignore (1)

48-48: Summary inconsistency: verify CLAUDE.md removal.

The AI summary states that the CLAUDE.md entry was removed from .gitignore, but the provided code segment shows only line 48 (.claude/) without showing the removal. Please verify that CLAUDE.md was indeed removed from the gitignore file to align with the PR objective of tracking it in version control.

docs/CLAUDE.md (1)

1-81: Well-organized Claude Code guidance documentation.

This new file provides comprehensive and well-structured guidance for Claude Code covering project architecture, routing, conventions, and integrations. The information is actionable and appropriately scoped.

@goldflag goldflag changed the title Enhance sitemap generation to include new tool pages and improve stru… add free tools Nov 23, 2025
- Updated multiple tool pages, including Analytics Detector, Bounce Rate Calculator, CTR Calculator, Funnel Visualizer, Marketing ROI Calculator, and UTM Builder, to use the new ToolPageLayout component for improved consistency and maintainability.
- Enhanced educational content and FAQs across these tools for better user engagement and clarity.
- Removed redundant imports and streamlined component structures to optimize performance and readability.
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: 3

♻️ Duplicate comments (5)
docs/src/app/sitemap.ts (1)

27-30: Error handling for directory operations is still missing.

Although a past review flagged this and marked it as addressed, the current code still lacks error handling. The readdirSync call will throw an exception if the directory doesn't exist or isn't readable, causing sitemap generation to fail.

Apply this diff to add error handling:

  const toolsPath = join(process.cwd(), "src/app/(home)/tools");
- const toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
-   .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
-   .map(dirent => dirent.name);
+ let toolSlugs: string[] = [];
+ try {
+   toolSlugs = readdirSync(toolsPath, { withFileTypes: true })
+     .filter(dirent => dirent.isDirectory() && dirent.name !== "components")
+     .map(dirent => dirent.name);
+ } catch (error) {
+   console.warn("Could not read tools directory:", error);
+ }
docs/src/app/(home)/tools/ctr-calculator/page.tsx (1)

22-38: Confirm intended canonical/organization domains in metadata and structured data.

Here the tool URLs (openGraph.url, alternates.canonical, and WebApplication @id/url) use https://rybbit.com/... while the structuredData author.url points to https://rybbit.io. That split (marketing site vs app/docs) might be intentional, but it would be good to confirm the canonical host you want search engines to associate with the tool and ensure the pattern is consistent across tool pages (CTR, bounce-rate, etc.).

You can quickly audit usage with:

#!/bin/bash
# Check tool and org domains used in tool metadata/structuredData
rg -n 'https://rybbit\.(com|io)' docs/src/app/(home)/tools --type=ts --type=tsx -C2

Also applies to: 45-60

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1)

6-34: Domain inconsistency remains unresolved across tool pages.

This file uses rybbit.io in metadata URLs (lines 24, 32) and structured data (line 43), while utm-builder/page.tsx in this same PR uses rybbit.com (lines 27, 36, 47). This fragmentation will cause SEO issues and break canonical URL expectations.

docs/src/app/(home)/tools/utm-builder/page.tsx (1)

9-21: Remove unsupported keywords field from metadata.

As noted in the previous review, the keywords field is not part of Next.js Metadata type and will be ignored. This field should be removed.

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1)

22-34: Social media images still missing from metadata.

As noted in the previous review, both openGraph and twitter metadata are missing image properties. The summary_large_image card type requires an image for proper social previews.

🧹 Nitpick comments (4)
docs/src/app/(home)/tools/MIGRATION_GUIDE.md (2)

185-190: Consider using the new barrel export in examples.

Now that docs/src/app/(home)/tools/components/index.ts re‑exports ToolPageLayout, the “After Migration” example could import from "../components" instead of "../components/ToolPageLayout" to keep docs aligned with the preferred public API.


356-362: Tighten wording in the benefits list.

“Exact same structure” is slightly wordy; consider “the same structure” or “a consistent structure” for a cleaner read.

docs/src/app/(home)/tools/analytics-detector/page.tsx (1)

1-7: Prefer import type { Metadata } for type‑only usage.

Metadata is only used as a type annotation, so switching to import type { Metadata } from "next"; would align with the other tool pages and avoid an unnecessary runtime import.

If your TS/Next config differs from the defaults, please double‑check that import type is supported as expected.

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1)

21-36: Verify domain split between tool URLs and author URL.

Here, the tool’s metadata and WebApplication url use https://rybbit.com/... while the structuredData author.url uses https://rybbit.io. If rybbit.io is strictly the app domain and rybbit.com is the canonical marketing/docs domain, this may be fine; otherwise, you may want to align these for SEO clarity and consistency with other tools.

Also applies to: 39-58

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77b1bae and 57e0c23.

📒 Files selected for processing (10)
  • docs/src/app/(home)/tools/MIGRATION_GUIDE.md (1 hunks)
  • docs/src/app/(home)/tools/analytics-detector/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1 hunks)
  • docs/src/app/(home)/tools/components/index.ts (1 hunks)
  • docs/src/app/(home)/tools/ctr-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/funnel-visualizer/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (1 hunks)
  • docs/src/app/(home)/tools/utm-builder/page.tsx (1 hunks)
  • docs/src/app/sitemap.ts (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
docs/src/app/(home)/tools/funnel-visualizer/page.tsx (2)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1)
  • ToolPageLayout (33-126)
docs/src/app/(home)/tools/funnel-visualizer/FunnelVisualizerForm.tsx (1)
  • FunnelVisualizerForm (20-230)
docs/src/app/(home)/tools/utm-builder/page.tsx (2)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1)
  • ToolPageLayout (33-126)
docs/src/app/(home)/tools/utm-builder/UTMBuilderForm.tsx (1)
  • UTMBuilderForm (8-233)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (4)
docs/src/app/(home)/tools/components/index.ts (3)
  • FAQItem (3-3)
  • ToolPageLayoutProps (3-3)
  • ToolPageLayout (2-2)
docs/src/components/Breadcrumbs.tsx (1)
  • Breadcrumbs (13-48)
docs/src/components/ui/accordion.tsx (4)
  • Accordion (58-58)
  • AccordionItem (58-58)
  • AccordionTrigger (58-58)
  • AccordionContent (58-58)
docs/src/components/RelatedTools.tsx (1)
  • RelatedTools (104-152)
docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (2)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1)
  • ToolPageLayout (33-126)
docs/src/app/(home)/tools/marketing-roi-calculator/MarketingROIForm.tsx (1)
  • MarketingROIForm (12-211)
docs/src/app/(home)/tools/ctr-calculator/page.tsx (3)
docs/src/app/(home)/tools/page.tsx (1)
  • metadata (18-22)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1)
  • ToolPageLayout (33-126)
docs/src/app/(home)/tools/ctr-calculator/CTRCalculatorForm.tsx (1)
  • CTRCalculatorForm (15-158)
docs/src/app/(home)/tools/analytics-detector/page.tsx (2)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx (1)
  • ToolPageLayout (33-126)
docs/src/app/(home)/tools/analytics-detector/AnalyticsDetectorForm.tsx (1)
  • AnalyticsDetectorForm (19-182)
🪛 ast-grep (0.40.0)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx

[warning] 50-50: 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)

🪛 Biome (2.1.2)
docs/src/app/(home)/tools/components/ToolPageLayout.tsx

[error] 51-51: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 LanguageTool
docs/src/app/(home)/tools/MIGRATION_GUIDE.md

[style] ~358-~358: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: .... Consistency: All tools follow the exact same structure 2. Maintainability: Updat...

(EN_WORDINESS_PREMIUM_EXACT_SAME)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Build Client Images (ubuntu-24.04-arm, linux/arm64)
  • GitHub Check: Build Client Images (ubuntu-latest, linux/amd64)
🔇 Additional comments (15)
docs/src/app/sitemap.ts (3)

4-5: LGTM!

The imports are appropriate for the new directory-reading functionality.


59-64: LGTM!

The new /tools static page entry is well-configured with appropriate priority and change frequency. Using new Date() for the tools landing page is acceptable since it serves as an index.


73-73: LGTM!

The return statement correctly includes the new toolPages in the sitemap with appropriate ordering.

docs/src/app/(home)/tools/components/index.ts (1)

1-3: Barrel exports look good.

Clean, minimal barrel that exposes the intended components and types; this will help keep imports consistent across tool pages.

docs/src/app/(home)/tools/ctr-calculator/page.tsx (1)

1-4: Imports and layout usage look solid.

Using import type { Metadata } and wiring ToolPageLayout with toolSlug, content blocks, FAQs, and structuredData matches the migration guide and the layout’s props contract.

docs/src/app/(home)/tools/analytics-detector/page.tsx (1)

288-303: ToolPageLayout integration is consistent and complete.

All required props (slug, title, description, toolComponent, educationalContent, faqs, relatedToolsCategory, CTA fields, structuredData) are provided and match the documented ToolPageLayout contract.

docs/src/app/(home)/tools/bounce-rate-calculator/page.tsx (1)

355-370: Bounce Rate Calculator page wiring looks correct.

ToolPageLayout is configured with the expected slug, content blocks, FAQ array, analytics category, CTA, and structuredData, matching the new tool-page pattern.

docs/src/app/(home)/tools/marketing-roi-calculator/page.tsx (2)

106-301: Educational content is comprehensive and well-structured.

The educationalContent block provides valuable information about ROI concepts, formulas, benchmarks, best practices, and optimization strategies. The structure is clear and accessible.


340-357: ToolPageLayout integration looks correct.

The default export properly delegates rendering to ToolPageLayout with all required props, aligning with the standardized tool page pattern across the PR.

docs/src/app/(home)/tools/utm-builder/page.tsx (1)

108-253: Educational content is thorough and helpful.

The UTM parameter explanations, naming conventions, use cases, and common mistakes are well-documented and provide practical guidance for users.

docs/src/app/(home)/tools/funnel-visualizer/page.tsx (2)

110-235: Educational content provides excellent funnel analysis guidance.

The content covers funnel concepts, stages, drop-off analysis methodology, and optimization strategies comprehensively. Well-organized and actionable.


275-292: ToolPageLayout integration is correct.

The component properly uses ToolPageLayout with all necessary props, matching the pattern established across other tool pages.

docs/src/app/(home)/tools/components/ToolPageLayout.tsx (3)

7-31: Interface definitions are clear and well-typed.

The FAQItem and ToolPageLayoutProps interfaces provide a clean contract for the layout component, with appropriate required/optional fields and ReactNode flexibility for content.


33-126: Component structure follows a logical, accessible layout.

The six-section layout (breadcrumbs, header, tool, educational content, FAQ, related tools, CTA) provides consistent structure across tool pages. The accordion implementation for FAQs is accessible, and the prose classes on educational content ensure good typography.


50-52: All structuredData props are safely statically-defined.

Verification confirms that every instance of structuredData passed to ToolPageLayout across all tool pages (utm-builder, ctr-calculator, bounce-rate-calculator, traffic-value-calculator, seo-title-generator, privacy-policy-builder, sample-size-calculator, meta-description-generator, funnel-visualizer, analytics-detector, og-tag-generator, and marketing-roi-calculator) uses hardcoded, module-level constant definitions following the pattern const structuredData = { "@context": "https://schema.org", "@graph": [...] }. No user input, dynamic data, or external API responses are being passed to dangerouslySetInnerHTML. The use of JSON.stringify() before injection is appropriate for this pattern.

description:
"Generate UTM campaign URLs instantly for accurate marketing tracking in Google Analytics and other analytics platforms.",
type: "website",
url: "https://rybbit.com/tools/utm-builder",
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

Domain inconsistency: this page uses rybbit.com while others use rybbit.io.

Lines 27, 36, and 47 use rybbit.com, but marketing-roi-calculator/page.tsx and funnel-visualizer/page.tsx in this PR use rybbit.io. Choose one canonical domain and apply it consistently across all tool metadata and structured data.

Also applies to: 36-36, 47-47

🤖 Prompt for AI Agents
In docs/src/app/(home)/tools/utm-builder/page.tsx around lines 27, 36 and 47,
the page uses the domain "rybbit.com" while other tool pages in this PR use
"rybbit.io"; update these three occurrences to the canonical domain (choose and
use "rybbit.io") so the metadata and structured data are consistent across all
tool pages, and verify no other instances remain in this file.

…al content

- Updated multiple tool pages, including Bounce Rate Calculator, Marketing ROI Calculator, and others, to utilize the ToolPageLayout component for improved consistency and maintainability.
- Enhanced educational content and FAQs across these tools for better user engagement and clarity.
- Removed redundant imports and streamlined component structures to optimize performance and readability.
- Modified multiple tool pages, including Bounce Rate Calculator, CTR Calculator, Funnel Visualizer, Marketing ROI Calculator, Page Speed Calculator, Traffic Value Calculator, and UTM Builder, to update URLs in metadata and structured data for consistency with the new domain.
- Enhanced educational content for clarity and improved readability across these tools.
- Ensured all references to the old domain are replaced to maintain functionality and SEO integrity.
…mprove SEO practices

- Updated the metadata for various tools, including CLAUDE, Analytics Detector, Bounce Rate Calculator, CTR Calculator, Funnel Visualizer, Marketing ROI Calculator, Meta Description Generator, Open Graph Tag Generator, Page Speed Calculator, Privacy Policy Builder, Sample Size Calculator, SEO Title Generator, and Traffic Value Calculator, by removing the keywords array.
- Enhanced educational content for clarity and improved readability across these tools.
- Ensured consistency in metadata structure to align with best SEO practices.
@goldflag goldflag merged commit b52dbaf into master Nov 23, 2025
8 checks passed
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.

2 participants