Skip to content

Comments

Proper SEO titles & descriptions#466

Merged
Blaumaus merged 8 commits intomainfrom
improvement/seo
Jan 26, 2026
Merged

Proper SEO titles & descriptions#466
Blaumaus merged 8 commits intomainfrom
improvement/seo

Conversation

@Blaumaus
Copy link
Member

@Blaumaus Blaumaus commented Jan 26, 2026

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed SSR hydration issue with password-required modal display on project pages
  • Improvements

    • Enhanced SEO metadata across all pages for improved search engine visibility and social media sharing previews
    • Implemented proper canonical URL handling to prevent duplicate content issues
    • Added complete page titles and descriptions for all pages, including password-protected projects

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

@Blaumaus Blaumaus self-assigned this Jan 26, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

Warning

Rate limit exceeded

@Blaumaus has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 56 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.

📝 Walkthrough

Walkthrough

This pull request refactors SEO metadata handling from a centralized SEO component to distributed React Router route-level meta functions. The SEO.tsx component is removed, App.tsx's title management logic is simplified, root.tsx now handles canonical URLs and meta tags directly, and route files gain new meta exports using translation hooks and SEO utilities.

Changes

Cohort / File(s) Summary
Core SEO Architecture Removal
web/app/components/SEO.tsx, web/app/utils/server.ts
SEO component entirely deleted (104 lines removed); getPageMeta function and GetPageMeta type removed from server.ts. Centralizes metadata logic removal.
App & Root Simplification
web/app/App.tsx, web/app/root.tsx
App.tsx removes useEffect, useTranslation, and complex route-based title logic; root.tsx adds canonical URL computation and direct meta tag injection (replaces SEO component).
Catch-all & Index Routes
web/app/routes/$.tsx, web/app/routes/_index.tsx
New meta functions using typed MetaFunction and i18n; $.tsx updates to destructured parameter pattern with translated fallbacks.
Blog Routes
web/app/routes/blog._index.tsx, web/app/routes/blog.$slug.tsx, web/app/routes/blog.$category.$slug.tsx
Meta functions added with i18n support; signatures updated to MetaFunction<typeof loader>; data-driven title/description composition.
Tool Routes
web/app/routes/tools._index.tsx, web/app/routes/tools.ctr-calculator.tsx, web/app/routes/tools.ip-lookup.tsx, web/app/routes/tools.roi-calculator.tsx, web/app/routes/tools.sitemap-validator.tsx, web/app/routes/tools.utm-generator.tsx
New meta exports with title, description, and preview image metadata; minimal translation integration varies by route.
Marketing/Feature Routes
web/app/routes/billing.tsx, web/app/routes/contact.tsx, web/app/routes/captcha._index.tsx, web/app/routes/captcha.demo.tsx, web/app/routes/error-tracking.tsx, web/app/routes/for-marketers.tsx, web/app/routes/for-small-businesses.tsx, web/app/routes/for-startups.tsx, web/app/routes/google-analytics-alternative.tsx, web/app/routes/open.tsx, web/app/routes/performance.tsx, web/app/routes/dashboard.tsx, web/app/routes/socialised.tsx
Meta functions added with SEO utilities and translation hooks; some routes add loader redirects and sitemap exclude logic.
User/Auth Routes
web/app/routes/login.tsx, web/app/routes/signup.tsx, web/app/routes/recovery.tsx, web/app/routes/verify.$id.tsx, web/app/routes/password-reset.$id.tsx, web/app/routes/change-email.$id.tsx
New meta exports with i18n; change-email.$id.tsx adds ChangeEmailLoaderData interface and sitemap function.
Project Routes
web/app/routes/projects.$id.tsx, web/app/routes/projects.settings.$id.tsx, web/app/routes/organisations.$id.tsx, web/app/routes/organisations._index.tsx, web/app/routes/organisation.invite.$id.tsx, web/app/routes/project.transfer.cancel.tsx, web/app/routes/project.transfer.confirm.tsx, web/app/routes/projects.$id.subscribers.invite.tsx, web/app/routes/share.$id.tsx
Meta functions with typed loader data; projects.$id.tsx and organisations.$id.tsx use dynamic data-driven titles; some add TITLE_SUFFIX composition.
Page Components
web/app/pages/Organisations/Settings/index.tsx, web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/pages/Project/View/ViewProject.tsx
Removed TITLE_SUFFIX usage and associated document.title side effects; ViewProject.tsx adds ClientOnly wrapper to PasswordRequiredModal for SSR hydration fix.
Route Config & Backend
web/app/routes/backend.$.tsx
Import formatting change; added inline comment on misconfiguration error handling.
SEO Utilities
web/app/utils/seo.ts
getTitle now accepts optional showSuffix parameter (default true); getPreviewImage accepts optional imageUrl with default value.
Localization
web/public/locales/en.json
Added titles.projectWithPassword and new description object with keys for various features (blog, captcha, contact, errorTracking, etc.).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • UI updates #441: Modifies web/app/utils/server.ts to remove getPageMeta function; this PR removes the entire SEO component architecture that depended on that function.
  • Goals analytics #446: Updates web/app/pages/Project/Settings/ProjectSettings.tsx and web/app/pages/Project/View/ViewProject.tsx for title management; overlaps with this PR's removal of TITLE_SUFFIX usage.
  • Free tool improvements #464: Modifies tool route files (tools._index.tsx, tools.ctr-calculator.tsx, etc.) for SEO enhancements; directly related to meta function additions in this PR.

Poem

🐰 From one great SEO home, we hoppers roam,
Each route now carries meta of its own,
No more central truth—now trust the way,
Each page knows best what tags to say!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing proper SEO titles and descriptions throughout the project.
Description check ✅ Passed The description follows the template structure with all required sections completed: Changes, Community Edition support, Database migrations, and Documentation checkboxes are appropriately marked.

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


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

@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: 12

🤖 Fix all issues with AI agents
In `@web/app/pages/Project/View/ViewProject.tsx`:
- Around line 696-703: The effect that updates document.title inside the
useEffect for live visitor counts exits early when user?.showLiveVisitorsInTitle
is false, leaving a previously-set "👀 …" title in place; update the effect in
the useEffect tied to project, user?.showLiveVisitorsInTitle, and liveVisitors
(the block that references document.title, liveVisitors, project, and
TITLE_SUFFIX) so that when showLiveVisitorsInTitle is false or project is
missing it explicitly resets document.title to the default (e.g., using
project.name + TITLE_SUFFIX or a safe fallback) instead of returning early.

In `@web/app/root.tsx`:
- Around line 316-324: The canonicalUrl builder currently includes the fragment
via the `hash` variable which can produce multiple canonical URLs; update the
`canonicalUrl` construction in root.tsx (the IIFE that uses `MAIN_URL`,
`pathname`, `search`, `hash`, `i18n.language`, and `defaultLanguage`) to omit
fragments—either stop concatenating `hash` into the URL or explicitly clear
`url.hash` before returning—then preserve the existing logic that sets or
deletes the `lng` search param based on `i18n.language`.
- Around line 335-343: Replace the single meta tag setting theme-color (the meta
with name='theme-color' currently using '#818cf8') with two theme-color meta
tags that target light and dark modes: add <meta name='theme-color'
media='(prefers-color-scheme: light)' content='#4f46e5' /> for indigo-600
(light) and <meta name='theme-color' media='(prefers-color-scheme: dark)'
content='#6366f1' /> for indigo-500 (dark); update the meta near the existing
meta lines (around the component that renders canonicalUrl and language) and
remove the old '#818cf8' tag.

In `@web/app/routes/captcha._index.tsx`:
- Around line 10-19: The meta function currently calls the React hook
useTranslation('common') which is invalid outside a component; replace that hook
usage in the exported meta function with a non-hook i18n call (use
i18next.getFixedT or the same pattern used in recovery.tsx) to obtain
translations, then build the return array with getTitle('CAPTCHA'),
getDescription(t('description.captcha')) and getPreviewImage() using that fixed
translator; update the meta function to call i18next.getFixedT('common') (or
equivalent) instead of useTranslation so the meta generation runs safely outside
React.

In `@web/app/routes/change-email`.$id.tsx:
- Around line 16-25: The meta() function is calling the React hook
useTranslation outside a component; remove that hook call and instead obtain
translations server-side (do not call hooks in meta). Concretely, stop using
useTranslation inside the exported meta, and either (A) compute the
title/description/preview values in the route loader (using your server-side
translation helper or i18n.getFixedT) and return them in loader data, then read
those values in meta({ data }) to call getTitle/getDescription/getPreviewImage,
or (B) import and call a non-hook server translation function (e.g., i18n.t or
getFixedT) directly inside meta to produce the same strings; update meta,
loader, and references to getTitle/getDescription/getPreviewImage accordingly.

In `@web/app/routes/contact.tsx`:
- Around line 10-19: The meta function incorrectly calls the React hook
useTranslation (in export const meta: MetaFunction) which must not run outside a
component; replace this by using a non-hook i18n accessor (e.g., i18next.t) or a
shared helper that calls i18next directly, then build the meta array via
getTitle, getDescription, getPreviewImage using those translated strings; create
a reusable utility (e.g., translateMeta or getServerT) that returns translations
for keys like 'titles.contact' and 'description.contact' and use that inside
meta instead of useTranslation to fix all route files.

In `@web/app/routes/organisations`.$id.tsx:
- Around line 11-28: The page meta currently builds title with TITLE_SUFFIX and
then calls getTitle (which also appends TITLE_SUFFIX), causing duplicate
suffixes; update the meta export (function meta) to build the base title without
TITLE_SUFFIX (e.g., const title = `${t('project.settings.settings')}
${orgName}`) and then pass that to getTitle so the suffix is only appended once;
locate the meta function in organisations.$id.tsx to apply this change.

In `@web/app/routes/projects`.$id.tsx:
- Around line 91-123: The meta function is incorrectly calling the React hook
useTranslation and parsing pathname with _split; remove the useTranslation()
call from meta, accept and use the params argument (use params.id) to build
pid/previewURL/canonicalURL instead of parsing location.pathname, and source any
localized strings from the loader-provided data (e.g., data.titles or
data.description keys) or fallback constants rather than calling hooks; update
references in meta (previewURL, canonicalURL, projectName, and any
getTitle/getDescription calls) to use params.id and data for
translations/fallbacks.

In `@web/app/routes/projects.settings`.$id.tsx:
- Around line 24-27: The title string currently includes TITLE_SUFFIX and then
calls getTitle(title) which appends the suffix again; fix by removing
TITLE_SUFFIX from the constructed title (change const title =
`${t('project.settings.settings')} ${projectName}`) or alternatively call
getTitle(title, false) to disable double-suffixing—update the code that sets the
title variable and the subsequent getTitle(...) call so only one TITLE_SUFFIX is
appended (refer to the title const, getTitle function, and TITLE_SUFFIX symbol).

In `@web/app/routes/recovery.tsx`:
- Around line 16-25: The meta export uses the React hook useTranslation inside
meta (function meta) which is invalid; replace the hook with the i18next
server-side translation call (e.g. import the i18next/i18n instance and call
i18next.t('titles.recovery') and i18next.t('description.recovery')) and remove
the eslint-disable comment, then pass those translated strings into getTitle,
getDescription, and getPreviewImage; update references in this file to use the
i18next/t function instead of useTranslation so meta remains a plain function
(affecting symbols: meta, useTranslation, getTitle, getDescription,
getPreviewImage).

In `@web/app/routes/verify`.$id.tsx:
- Around line 12-21: The meta export is calling the React hook useTranslation
outside a component (in export const meta: MetaFunction), so replace hook usage
with a non-hook i18n getter: add a shared helper getMetaTranslation that returns
i18next.getFixedT(null, namespace) (or similar) and then in verify.$id.tsx
change the meta function to call that helper (const t = getMetaTranslation())
and continue to use getTitle, getDescription, and getPreviewImage as before;
ensure the helper is imported where meta is defined and remove the
useTranslation import from this file.

In `@web/public/locales/en.json`:
- Around line 2101-2109: The description key for the signup page is misspelled
as "singup", causing the meta in web/app/routes/signup.tsx to resolve to a
missing description; rename the key inside the "description" object from
"singup" to "signup" and update the same key in all other locale files to keep
translations in sync so the signup route meta correctly reads
description.signup.
🧹 Nitpick comments (3)
web/app/utils/seo.ts (1)

3-14: Reduce repeated title string construction.
This avoids duplication and makes future edits less error‑prone.

♻️ Suggested refactor
-export const getTitle = (title: string, showSuffix = true) => [
-  {
-    title: `${title} ${showSuffix ? TITLE_SUFFIX : ''}`.trim(),
-  },
-  {
-    property: 'og:title',
-    content: `${title} ${showSuffix ? TITLE_SUFFIX : ''}`.trim(),
-  },
-  {
-    name: 'twitter:title',
-    content: `${title} ${showSuffix ? TITLE_SUFFIX : ''}`.trim(),
-  },
-]
+export const getTitle = (title: string, showSuffix = true) => {
+  const fullTitle = `${title} ${showSuffix ? TITLE_SUFFIX : ''}`.trim()
+
+  return [
+    { title: fullTitle },
+    { property: 'og:title', content: fullTitle },
+    { name: 'twitter:title', content: fullTitle },
+  ]
+}
web/app/routes/tools._index.tsx (1)

12-14: Consider adding a meta description for SEO consistency.

Unlike other tool routes (tools.utm-generator.tsx, tools.roi-calculator.tsx), this route is missing a getDescription() call. Adding a description improves SEO and social sharing previews.

♻️ Suggested improvement
 export const meta: MetaFunction = () => {
-  return [...getTitle('Free Marketing Tools'), ...getPreviewImage()]
+  return [
+    ...getTitle('Free Marketing Tools'),
+    ...getDescription(
+      'Professional marketing calculators and generators to optimize your campaigns, track performance, and maximize ROI. No sign-up required.',
+    ),
+    ...getPreviewImage(),
+  ]
 }

You'll also need to add getDescription to the import:

-import { getPreviewImage, getTitle } from '~/utils/seo'
+import { getDescription, getPreviewImage, getTitle } from '~/utils/seo'
web/app/routes/reports-unsubscribe.$token.tsx (1)

12-20: Consider localizing the title for i18n parity.

Other routes use translated titles; this page’s meta title will stay English in non‑English locales. Consider moving it to the common locale files.

♻️ Suggested change
-    ...getTitle('Unsubscribe from email reports'),
+    ...getTitle(t('titles.reportsUnsubscribe')),

@Blaumaus Blaumaus merged commit 713b076 into main Jan 26, 2026
6 of 7 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Feb 7, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant