Skip to content

feat(seo): improve SERP appearance with per-page metadata and snippet suppression#799

Open
ImJustChew wants to merge 1 commit intomainfrom
fix/serp-appearance
Open

feat(seo): improve SERP appearance with per-page metadata and snippet suppression#799
ImJustChew wants to merge 1 commit intomainfrom
fix/serp-appearance

Conversation

@ImJustChew
Copy link
Copy Markdown
Member

Summary

  • Worker STATIC_PAGE_METADATA: all major static pages (/courses, /timetable, /today, /calendar, /bus, /venues, /sports-venues, /chat, /shops, /apps, /team, /contribute) now get page-specific <title> and <meta name="description"> injected for bot requests, replacing the generic index.html placeholder
  • data-nosnippet on error boundaries: error message text (e.g. "Failed to fetch dynamically imported module: https://…") and the empty pinned-apps hint ("Pin your most used apps here!") were leaking into Google sitelink snippets — suppressed with data-nosnippet
  • hreflang consistency: handleBusPage and handleDepartmentPage now call applyHreflang() like course pages do; SEOHead.tsx attribute corrected from hreflanghrefLang with zh-TW locale

Test plan

  • Visit /zh/courses, /zh/timetable, /zh/bus, etc. with a bot UA (curl -A "Googlebot") and verify <title> and <meta description> match STATIC_PAGE_METADATA
  • Verify <link rel="alternate" hreflang="zh-TW"> / hreflang="en" appear on all bot-served pages
  • Confirm error boundary pages have data-nosnippet attribute
  • Check Google Search Console for snippet improvements after recrawl

🤖 Generated with Claude Code

… suppression

- Worker: inject page-specific title + description for all static pages
  (courses, timetable, today, calendar, bus, venues, sports-venues, chat,
  shops, apps, team, contribute) when bots visit, via STATIC_PAGE_METADATA
- Worker: applyHreflang() helper now wired into handleBusPage and
  handleDepartmentPage (was missing), ensuring correct zh-TW/en alternates
- Worker: buildCourseMetaData now returns zhUrl/enUrl; handleCourseDetailPage
  uses applyHreflang() instead of static hreflang attributes
- error.tsx (root + mods-pages): add data-nosnippet so error messages
  (e.g. "Failed to fetch dynamically imported module: ...") are excluded
  from Google snippets
- apps/page.tsx: add data-nosnippet to empty pinned-apps hint to prevent
  it appearing as sitelink description
- SEOHead.tsx: fix hreflang → hrefLang (JSX attribute) and use zh-TW locale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 27, 2026 09:42
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 27, 2026

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

Project Deployment Actions Updated (UTC)
courseweb Ready Ready Preview Apr 27, 2026 9:42am

Request Review

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
52.2% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
courseweb-web 94da594 Commit Preview URL

Branch Preview URL
Apr 27 2026, 09:43 AM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Improves bot-visible SEO signals by injecting per-route metadata into the HTML shell in the Cloudflare Worker, standardizing hreflang handling across more routes, and suppressing unwanted snippet text via data-nosnippet.

Changes:

  • Add per-static-page <title> / description injection for bot requests via STATIC_PAGE_METADATA and a generic bot handler in the Worker.
  • Apply consistent hreflang rewriting for course, department, and bus bot-served pages.
  • Add data-nosnippet to error boundaries and the empty pinned-apps reminder; fix SEOHead JSX attribute to hrefLang (and use zh-TW).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
apps/web/worker.ts Adds generic bot-page rewriting, per-page metadata table, and shared hreflang rewriting helper.
apps/web/src/components/SEOHead.tsx Fixes JSX hreflang attribute usage by switching to hrefLang and updates locale tag to zh-TW.
apps/web/src/app/error.tsx Adds data-nosnippet to suppress error page content from SERP snippets.
apps/web/src/app/[lang]/(mods-pages)/error.tsx Adds data-nosnippet around route-level error boundary content.
apps/web/src/app/[lang]/(mods-pages)/apps/page.tsx Adds data-nosnippet to the empty pinned-apps reminder text.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/web/worker.ts
Comment on lines +532 to +540
const pathname = url.pathname;
const lang = pathname.startsWith("/en/") || pathname === "/en" ? "en" : "zh";

// Canonical = current path with no query params
const canonicalUrl = `https://nthumods.com${pathname}`;
const zhPath = pathname.replace(/^\/(zh|en)(\/|$)/, "/zh$2");
const enPath = pathname.replace(/^\/(zh|en)(\/|$)/, "/en$2");
const zhUrl = `https://nthumods.com${zhPath}`;
const enUrl = `https://nthumods.com${enPath}`;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

handleGenericBotPage() sets canonicalUrl directly from pathname. For locale roots, this means /zh and /zh/ (and /en vs /en/) will produce different canonicals depending on the request URL, which can lead to duplicate indexing. Since this worker already treats /zh/ and /en/ as the canonical forms in the sitemap generation, consider normalizing pathname so locale roots always canonicalize to a single form (e.g. enforce a trailing slash).

Copilot uses AI. Check for mistakes.
Comment thread apps/web/worker.ts
Comment on lines +550 to +558
let rewriter = new HTMLRewriter().on('link[rel="canonical"]', {
element(el) {
el.setAttribute("href", canonicalUrl);
},
});

rewriter = applyHreflang(rewriter, zhUrl, enUrl, zhUrl);

if (meta) {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

handleGenericBotPage() always rewrites , but it only rewrites meta[property="og:url"] when a pagePath exists in STATIC_PAGE_METADATA. For pages not in that table (e.g. /zh/settings), bots will see a canonical pointing at the page while og:url remains the generic index.html value (https://nthumods.com). Consider always rewriting og:url to canonicalUrl regardless of whether meta is found.

Copilot uses AI. Check for mistakes.
Comment thread apps/web/worker.ts
const enUrl = `https://nthumods.com${enPath}`;

// Look up page-specific metadata by stripping the lang prefix
const pagePath = pathname.replace(/^\/(zh|en)/, "") || "/";
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

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

handleGenericBotPage() looks up STATIC_PAGE_METADATA using the raw pathname after stripping the lang prefix. If a bot hits a trailing-slash variant (e.g. /zh/apps/), pagePath becomes "/apps/" and the lookup will miss, leaving the generic title/description. Consider normalizing pagePath by removing a trailing "/" (except for "/") before indexing STATIC_PAGE_METADATA.

Suggested change
const pagePath = pathname.replace(/^\/(zh|en)/, "") || "/";
const rawPagePath = pathname.replace(/^\/(zh|en)/, "") || "/";
const pagePath =
rawPagePath.length > 1 && rawPagePath.endsWith("/")
? rawPagePath.slice(0, -1)
: rawPagePath;

Copilot uses AI. Check for mistakes.
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