Skip to content

feat: add SEO - #3

Merged
flvvius merged 3 commits into
mainfrom
feat/add-SEO
Feb 25, 2026
Merged

feat: add SEO#3
flvvius merged 3 commits into
mainfrom
feat/add-SEO

Conversation

@flvvius

@flvvius flvvius commented Feb 25, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Enhanced SEO across the site with dynamic titles, descriptions, Open Graph and Twitter Card tags.
    • Added structured data (JSON-LD) and canonical links to improve search indexing.
    • Route-level metadata including page-specific robots directives (e.g., dashboard, unsubscribe) and event-specific meta when available.
  • Chores

    • Updated robots.txt with crawl rules and sitemap reference.
    • Added and preloaded a self-hosted Inter variable font for improved typography and performance.
  • Documentation

    • Reformatted design system markdown tables for consistency.

@vercel

vercel Bot commented Feb 25, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
news-web Ready Ready Preview, Comment Feb 25, 2026 7:52pm

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Introduces site-wide SEO and fonts: adds SITE config, updates robots.txt, self-hosted Inter font, route-level head() metadata (OG/Twitter/canonical and JSON-LD), and an event route loader for server-side event data and dynamic metadata.

Changes

Cohort / File(s) Summary
Robots & SEO config
apps/web/public/robots.txt, apps/web/src/lib/seo.ts
robots.txt now allows root, disallows /dashboard, /api/, /unsubscribe, and adds a Sitemap. Added exported SITE constant with site metadata (name, url, title, description, ogImage).
Root layout & fonts
apps/web/src/routes/__root.tsx, apps/web/src/index.css
__root.tsx now imports SITE, replaces static head tags with SITE-driven meta (OG/Twitter/theme-color), adds stylesheet/link for app CSS and preloads the Inter font, and wraps content in <main>. index.css adds two @font-face blocks to self-host Inter (latin, latin-ext).
Route-level SEO metadata
apps/web/src/routes/index.tsx, apps/web/src/routes/feed.tsx, apps/web/src/routes/unsubscribe.tsx, apps/web/src/routes/dashboard.tsx
Added head() functions to inject meta and link tags; index includes JSON-LD WebSite structured data; feed and index add canonical/meta tags; dashboard and unsubscribe add robots: noindex, nofollow.
Event route: loader & dynamic head
apps/web/src/routes/event.$slug.tsx
Added a server-side loader that fetches event by slug via serverHttpClient (returns null on mismatch/error) and a head() that builds title, description, OG/Twitter image tags, and canonical link based on loader data and params.
Docs formatting
apps/web/DESIGN_SYSTEM.md
Large markdown table reformatting (alignment/spacing) with no semantic content changes.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Route as EventRoute
  participant ServerClient as serverHttpClient
  participant DataStore as ExternalAPI

  Client->>Route: GET /event/:slug
  Route->>ServerClient: loader fetchEventBySlug(slug)
  ServerClient->>DataStore: request event data
  DataStore-->>ServerClient: return event payload
  ServerClient-->>Route: loaderData (event)
  Route-->>Client: render page + dynamic head/meta (title, description, OG, canonical)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add SEO' accurately summarizes the main change—adding comprehensive SEO configuration across the application including robots.txt updates, SEO metadata module, and per-route metadata with Open Graph and Twitter tags.

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

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/routes/__root.tsx`:
- Around line 58-67: Replace the external Google Fonts link tags (the elements
with href "https://fonts.googleapis.com/css2?family=Inter:..." and the
preconnects) in __root.tsx with references to a locally-served font bundle; add
Inter font files to the app (woff2/woff) and declare them via `@font-face` in your
global CSS (or a dedicated fonts.css), then import that CSS from the root (where
the current stylesheet link is added) and optionally add a local <link
rel="preload" as="font"> for the most-critical weights; update/remove the
crossOrigin preconnect entries and ensure the code paths that render the three
link objects (the objects with rel "preconnect" and rel "stylesheet") now point
to your local assets so the app self-hosts Inter instead of depending on
fonts.googleapis.com.

In `@apps/web/src/routes/event`.$slug.tsx:
- Around line 16-22: The loader currently swallows all exceptions when calling
httpClient.query(api.events.getEventBySlug, { slug: params.slug }) and returns
null; change the catch block to log or report the caught error (e.g.,
console.error or your app error reporter/Sentry) including the error object and
context (slug) before returning the fallback, so failures during SSR/backend
calls are visible for debugging; keep the return null fallback but ensure the
catch in the event.$slug route exposes the error via logging/reporting.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a5efe4 and 65ca45f.

📒 Files selected for processing (8)
  • apps/web/public/robots.txt
  • apps/web/src/lib/seo.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/event.$slug.tsx
  • apps/web/src/routes/feed.tsx
  • apps/web/src/routes/index.tsx
  • apps/web/src/routes/unsubscribe.tsx

Comment thread apps/web/src/routes/__root.tsx Outdated
Comment on lines 58 to 67
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: appCss,
href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider self-hosting Inter to reduce third-party critical-path cost.

Line 66 introduces a render-path dependency on Google Fonts. Self-hosting Inter (or bundling via local @font-face) usually improves first paint consistency and removes third-party dependency from initial render.

As per coding guidelines, "apps/web/**: - Focus on web performance and accessibility".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/__root.tsx` around lines 58 - 67, Replace the external
Google Fonts link tags (the elements with href
"https://fonts.googleapis.com/css2?family=Inter:..." and the preconnects) in
__root.tsx with references to a locally-served font bundle; add Inter font files
to the app (woff2/woff) and declare them via `@font-face` in your global CSS (or a
dedicated fonts.css), then import that CSS from the root (where the current
stylesheet link is added) and optionally add a local <link rel="preload"
as="font"> for the most-critical weights; update/remove the crossOrigin
preconnect entries and ensure the code paths that render the three link objects
(the objects with rel "preconnect" and rel "stylesheet") now point to your local
assets so the app self-hosts Inter instead of depending on fonts.googleapis.com.

Comment thread apps/web/src/routes/event.$slug.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/index.css`:
- Line 10: The `@font-face` declarations use quoted single-word font family names
(e.g., "Inter") which violates the stylelint rule; update the font-family
properties inside each `@font-face` block (look for font-family in the file) to
remove the quotes so they read Inter (and likewise for any other single-word
names on the other occurrence), keeping spacing and capitalization unchanged.

In `@apps/web/src/routes/event`.$slug.tsx:
- Around line 44-45: When constructing canonical and og:url metadata in
event.$slug.tsx, encode params.slug to avoid invalid URLs; update the places
that build `${SITE.url}/event/${params.slug}` (including the og:url and
canonical link construction) to use an encoded slug (e.g., via
encodeURIComponent(params.slug)) so reserved characters are percent-encoded and
URLs remain valid.
- Around line 38-51: The route's meta array for event.$slug.tsx adds og:* and
twitter:image but omits route-specific twitter:title and twitter:description, so
add { name: "twitter:title", content: title } and { name: "twitter:description",
content: description } into the meta array (near the existing
og:title/og:description entries and alongside the conditional twitter:image) so
that Twitter cards use the event's title and description; reference the meta
array, the title and description variables, and the imageUrl conditional when
inserting these tags.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65ca45f and 3c8fd5f.

⛔ Files ignored due to path filters (2)
  • apps/web/public/fonts/inter-latin-ext-wght-normal.woff2 is excluded by !**/*.woff2
  • apps/web/public/fonts/inter-latin-wght-normal.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (5)
  • apps/web/DESIGN_SYSTEM.md
  • apps/web/src/index.css
  • apps/web/src/routes/__root.tsx
  • apps/web/src/routes/dashboard.tsx
  • apps/web/src/routes/event.$slug.tsx

Comment thread apps/web/src/index.css
Self-hosted Inter variable font (latin + latin-ext)
────────────────────────────────────────────── */
@font-face {
font-family: "Inter";

Copy link
Copy Markdown
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

🏁 Script executed:

# Check if the file exists and view content around the reported lines
cat -n apps/web/src/index.css | head -35

Repository: flvvius/news

Length of output: 1487


🏁 Script executed:

# Search for stylelint configuration files
fd -e '.stylelintrc' -e 'stylelint.config' -e '.stylelintignore' .

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Check package.json for stylelint configuration
find . -name "package.json" -type f | xargs grep -l "stylelint" | head -5

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Search for package.json and check for stylelint
find . -maxdepth 3 -name "package.json" | head -3 | xargs cat | jq '.devDependencies, .stylelint' 2>/dev/null

Repository: flvvius/news

Length of output: 505


🏁 Script executed:

# Check root directory for stylelint config
ls -la . | grep -i stylelint

Repository: flvvius/news

Length of output: 124


🏁 Script executed:

# Search for any stylelint configuration in common locations
cat .stylelintrc.json 2>/dev/null || cat .stylelintrc.js 2>/dev/null || cat stylelint.config.js 2>/dev/null || echo "No stylelint config found in root"

Repository: flvvius/news

Length of output: 1062


🏁 Script executed:

# Check the web app's specific package.json for stylelint
cat apps/web/package.json | jq '.devDependencies' 2>/dev/null | grep -i stylelint

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Search for stylelint in any package.json files
find . -name "package.json" -type f -exec grep -l "stylelint" {} \;

Repository: flvvius/news

Length of output: 38


🏁 Script executed:

# Check the root package.json more thoroughly
cat package.json | jq '.devDependencies, .dependencies' 2>/dev/null | head -50

Repository: flvvius/news

Length of output: 82


🌐 Web query:

stylelint-config-standard-scss font-family-name-quotes default rule

💡 Result:

In stylelint-config-standard-scss, font-family-name-quotes defaults to:

  • "always-where-recommended" (inherited from stylelint-config-standard; it’s not overridden in stylelint-config-standard-scss). [1]

What that option means: it requires quotes only where the CSS spec recommends them (e.g., names with spaces/digits/punctuation) and disallows quotes otherwise. [2]

Sources
[1] stylelint-config-standard config (font-family-name-quotes: 'always-where-recommended') — (raw.githubusercontent.com)
[2] Stylelint rule docs for font-family-name-quotes option "always-where-recommended" — (stylelint.io)

Citations:


Remove quotes around font-family names in @font-face blocks.

Lines 10 and 21 violate the stylelint rule font-family-name-quotes: always-where-recommended. Since "Inter" is a single-word name requiring no quotes per the CSS spec, the quotes must be removed.

Proposed fix
 `@font-face` {
-  font-family: "Inter";
+  font-family: Inter;
   font-style: normal;
   font-weight: 100 900;
   font-display: swap;
   src: url("/fonts/inter-latin-wght-normal.woff2") format("woff2");
 }

 `@font-face` {
-  font-family: "Inter";
+  font-family: Inter;
   font-style: normal;
   font-weight: 100 900;
   font-display: swap;
   src: url("/fonts/inter-latin-ext-wght-normal.woff2") format("woff2");
 }

Also applies to: 21-21

🧰 Tools
🪛 Stylelint (17.3.0)

[error] 10-10: Unexpected quotes around "Inter" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/index.css` at line 10, The `@font-face` declarations use quoted
single-word font family names (e.g., "Inter") which violates the stylelint rule;
update the font-family properties inside each `@font-face` block (look for
font-family in the file) to remove the quotes so they read Inter (and likewise
for any other single-word names on the other occurrence), keeping spacing and
capitalization unchanged.

Comment thread apps/web/src/routes/event.$slug.tsx
Comment on lines +44 to +45
{ property: "og:url", content: `${SITE.url}/event/${params.slug}` },
...(imageUrl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Encode slug when building canonical and og:url metadata.

If params.slug contains reserved characters, these URLs can become invalid or inconsistent.

♻️ Proposed fix
+    const encodedSlug = encodeURIComponent(params.slug);

     return {
       meta: [
         { title },
         { name: "description", content: description },
         { property: "og:title", content: title },
         { property: "og:description", content: description },
         { property: "og:type", content: "article" },
-        { property: "og:url", content: `${SITE.url}/event/${params.slug}` },
+        { property: "og:url", content: `${SITE.url}/event/${encodedSlug}` },
         ...(imageUrl
           ? [
               { property: "og:image", content: imageUrl },
               { name: "twitter:image", content: imageUrl },
             ]
           : []),
       ],
-      links: [{ rel: "canonical", href: `${SITE.url}/event/${params.slug}` }],
+      links: [{ rel: "canonical", href: `${SITE.url}/event/${encodedSlug}` }],
     };

Also applies to: 52-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/event`.$slug.tsx around lines 44 - 45, When constructing
canonical and og:url metadata in event.$slug.tsx, encode params.slug to avoid
invalid URLs; update the places that build `${SITE.url}/event/${params.slug}`
(including the og:url and canonical link construction) to use an encoded slug
(e.g., via encodeURIComponent(params.slug)) so reserved characters are
percent-encoded and URLs remain valid.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
apps/web/src/routes/event.$slug.tsx (1)

44-45: ⚠️ Potential issue | 🟡 Minor

Encode params.slug in og:url and canonical URLs.

Line 44 and Line 54 still use raw params.slug; reserved characters can produce invalid/inconsistent URLs.

♻️ Proposed fix
   head: ({ loaderData, params }) => {
+    const encodedSlug = encodeURIComponent(params.slug);
     const title = loaderData?.event?.title
       ? `${loaderData.event.title} — ${SITE.name}`
       : `Event — ${SITE.name}`;
@@
-        { property: "og:url", content: `${SITE.url}/event/${params.slug}` },
+        { property: "og:url", content: `${SITE.url}/event/${encodedSlug}` },
@@
-      links: [{ rel: "canonical", href: `${SITE.url}/event/${params.slug}` }],
+      links: [{ rel: "canonical", href: `${SITE.url}/event/${encodedSlug}` }],
     };
   },

Also applies to: 54-54

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/event`.$slug.tsx around lines 44 - 45, The og:url and
canonical URL are currently built using raw params.slug which can contain
reserved characters; update the places that set the meta property { property:
"og:url", content: `${SITE.url}/event/${params.slug}` } and the canonical link
(the link with rel="canonical") to encode the slug using
encodeURIComponent(params.slug) when composing the URL so both meta tags produce
valid, safe URLs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/routes/event`.$slug.tsx:
- Around line 44-45: The og:url and canonical URL are currently built using raw
params.slug which can contain reserved characters; update the places that set
the meta property { property: "og:url", content:
`${SITE.url}/event/${params.slug}` } and the canonical link (the link with
rel="canonical") to encode the slug using encodeURIComponent(params.slug) when
composing the URL so both meta tags produce valid, safe URLs.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c8fd5f and 7c2a42e.

📒 Files selected for processing (1)
  • apps/web/src/routes/event.$slug.tsx

@flvvius
flvvius merged commit 4f328b3 into main Feb 25, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 26, 2026
@coderabbitai coderabbitai Bot mentioned this pull request May 10, 2026
This was referenced Jul 4, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 12, 2026
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