mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
<!-- ccr-slack-attribution --> _Requested by **Pam Chia** · [Slack thread](https://supabase.slack.com/archives/C07P3AU3J2D/p1787616624076769)_ **Before:** searching "supabase blog" on Google surfaces blog tag sitelinks titled `Blog/Tags/Supabase` and `Blog/Tags/Community`, each with the same snippet scraped from the footer newsletter form ("Get product updates and news from Supabase"). **After:** those results use the route's real title, `Blog | Supabase`, with a description specific to the tag, for example "Blog posts tagged Supabase." This change stops the visible breadcrumb on blog tag and author pages from being the page's only `<h1>`, and gives tag and category pages distinct meta descriptions. ## How this changes the Google result The bad sitelinks come from two independent mechanisms, and each half of this PR targets one of them: **Titles.** Google prefers a page's `<h1>` over its `<title>` when the two disagree, and on tag pages the only `<h1>` is the breadcrumb, whose textContent is exactly `Blog/Tags/Supabase` (JSX strips the whitespace between the children). Demoting the breadcrumb to a `<nav>` removes the conflicting heading, so Google should fall back to the route's real `<title>`: - `Blog/Tags/Supabase` becomes `Blog | Supabase` - `Blog/Tags/Community` becomes `Blog | Community` **Snippets.** The "Get product updates and news from Supabase. Subscribe ..." text is not a meta description; it is Google's own synthesized snippet, scraped from the footer newsletter form. Every tag and category page shipped the byte-identical description "Latest news from the Supabase team.", and Google discards a description duplicated across many URLs. With a unique per-page description, Google has a usable candidate again: - tag rows should show `Blog posts tagged Supabase.` / `Blog posts tagged Community.` - category rows (`Blog | Engineering`, `Blog | Product`) already had correct titles but the same scraped snippet; they should now show `Blog posts in Engineering.` / `Blog posts in Product.` Two caveats. Meta descriptions are suggestions, not directives, so Google can still synthesize its own snippet; removing the duplicate-description cause makes the supplied one much more likely to win, but does not force it. And the replacement `<h1>` is the constant sr-only `Supabase Blog` on every listing page: if Google again prefers the h1 over the title, all sitelinks would read `Supabase Blog`. A page-specific heading ("Posts tagged Supabase", "Posts by <author>") would eliminate that residual risk and is a candidate follow-up. Neither change appears in search results until Google recrawls and reprocesses these URLs (see Additional context; the tag routes are absent from the sitemap, which slows this). Requesting reindexing of a few of these URLs in Search Console would speed it up. ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix (SEO / accessibility), `apps/www` only. ## What is the current behavior? Two separate defects feed the same bad search result. Google prefers a page's `<h1>` over its `<title>` when the two disagree, and on tag pages the only `<h1>` is the breadcrumb. `apps/www/app/blog/tags/[tag]/TagClient.tsx:21-27` wraps `Blog` / `/` / `Tags` / `/` / `<tag>` in an `<h1>`; JSX strips the whitespace between those children, so the element's textContent is exactly `Blog/Tags/Supabase`. The route's own `<title>` is already fine (`apps/www/app/blog/tags/[tag]/page.tsx:26`). `apps/www/app/blog/authors/[author]/AuthorClient.tsx:55-60` has the identical defect. Category pages escape it because `apps/www/app/blog/BlogLayoutShell.tsx:19` counts `/blog/categories/` as a listing route and renders the sr-only `<h1>Supabase Blog</h1>` at line 23, while `CategoryClient.tsx` renders no `<h1>` of its own. Separately, every tag and category page shipped the byte-identical description `Latest news from the Supabase team.` (`apps/www/app/blog/tags/[tag]/page.tsx:27`, `apps/www/app/blog/categories/[category]/page.tsx:23`). Google discards a description reused verbatim across many pages and generates its own snippet, in this case from the footer newsletter copy at `apps/www/components/Footer/index.tsx:179`. ## What is the new behavior? - `BlogLayoutShell.tsx` — `isListingRoute` now covers `/blog/tags/` and `/blog/authors/` alongside `/blog/categories/`, via a `LISTING_ROUTE_PREFIXES` constant. Those routes now render the same sr-only `<h1>Supabase Blog</h1>` category pages already had. - `TagClient.tsx` and `AuthorClient.tsx` — the breadcrumb is now a `<nav aria-label="Breadcrumb">` instead of an `<h1>`. Every existing className, the `/` separator spans and their `px-2` padding are unchanged. The `h1` element selector in `apps/www/styles/globals.css:176-179` applies `font-heading font-medium tracking-normal`, so those three utilities move onto the `nav` to keep the rendering byte-identical. No visual change is intended; only the element and its accessible role change. - `tags/[tag]/page.tsx` and `categories/[category]/page.tsx` — `generateMetadata` now returns `Blog posts tagged ${label}.` and `Blog posts in ${label}.`, matching the shape the author route already uses (`apps/www/app/blog/authors/[author]/page.tsx:37`). Both use `startCase` from `apps/www/lib/helpers.tsx:75-81` rather than `capitalize`, so `launch-week` reads "Launch Week" and matches the filter chip label at `apps/www/components/Blog/BlogFilters.tsx:56-57`. Title and description use the same label. ## Additional context `apps/www` is owned by `@supabase/marketing` per `.github/CODEOWNERS:13`, so marketing should review this. Recovery is not immediate: Google has to recrawl these routes before the corrected titles and descriptions show up in search results. Noted follow-ups, deliberately out of scope here: - `/blog/tags/*` and `/blog/authors/*` are absent from the generated sitemap (`apps/www/internals/generate-sitemap.mjs`), which slows discovery and recrawl. - The page bodies still pass a `capitalize`-derived label to `TagClient` and `CategoryClient`, so the visible tag breadcrumb reads "Launch week" while the title now reads "Launch Week". Aligning the visible label would be a rendered-text change, so it is left for a separate PR. - `openGraph` metadata on these routes is untouched and still carries the generic copy. ### How it was tested Prettier passes on the five changed files with the repo config, in both the default and the `SORT_IMPORTS=false` mode CI uses. Full typecheck and lint could not be run in this environment (no `node_modules`); the changes are type-trivial — a `string[]`.`some()` predicate, a JSX tag swap, and two template literals over an existing exported `startCase(string): string` helper — and the diff parses clean under `tsc` with module resolution disabled. Worth a reviewer eyeballing the tag and author pages side by side against production to confirm the breadcrumb renders identically. --- _Generated by [Claude Code](https://claude.ai/code/session_0164goAzGTkGnoxzCKagJ3Hw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
35 lines
1.1 KiB
TypeScript
35 lines
1.1 KiB
TypeScript
'use client'
|
|
|
|
import { usePathname } from 'next/navigation'
|
|
import type PostTypes from 'types/post'
|
|
|
|
import BlogHero from './BlogHero'
|
|
import DefaultLayout from '@/components/Layouts/Default'
|
|
|
|
// Listing routes have no page-specific title of their own, so they share the
|
|
// generic screen-reader heading below. Without it the visible breadcrumb ends up
|
|
// as the only heading on the page, and search engines pick that up as the title.
|
|
const LISTING_ROUTE_PREFIXES = ['/blog/categories/', '/blog/tags/', '/blog/authors/']
|
|
|
|
export default function BlogLayoutShell({
|
|
featuredPost,
|
|
secondaryPosts,
|
|
children,
|
|
}: {
|
|
featuredPost: PostTypes | null
|
|
secondaryPosts: PostTypes[]
|
|
children: React.ReactNode
|
|
}) {
|
|
const pathname = usePathname()
|
|
const isListingRoute =
|
|
pathname === '/blog' || LISTING_ROUTE_PREFIXES.some((prefix) => pathname?.startsWith(prefix))
|
|
|
|
return (
|
|
<DefaultLayout>
|
|
{isListingRoute && <h1 className="sr-only">Supabase Blog</h1>}
|
|
<BlogHero featuredPost={featuredPost} secondaryPosts={secondaryPosts} />
|
|
{children}
|
|
</DefaultLayout>
|
|
)
|
|
}
|