Skip to content

fix(seo): absolute og:image and single canonical - #77

Closed
pamelachia wants to merge 2 commits into
multigres:mainfrom
pamelachia:pamela/growth-1208-social-previews-seo-meta-tags
Closed

fix(seo): absolute og:image and single canonical#77
pamelachia wants to merge 2 commits into
multigres:mainfrom
pamelachia:pamela/growth-1208-social-previews-seo-meta-tags

Conversation

@pamelachia

@pamelachia pamelachia commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Shared links to multigres.com rendered on X, Slack, and LinkedIn as text-only cards. Every page emitted og:image and twitter:image as the relative path /img/og-image.png, which unfurlers reject, so the site never showed a preview image despite declaring summary_large_image. The same head() functions also carried a duplicate canonical on every page and left the blog index without its own metadata, so I fixed both here too.

Changed:

  • Social previews render: og:image and twitter:image use the absolute defaultOgImage from src/lib/shared.ts. A blog post's frontmatter image is resolved through the URL parser (root-relative, rootless, protocol-relative, and absolute values all resolve correctly) before it reaches og:image and JSON-LD.
  • One canonical per page: the root layout no longer sets canonical or og:url. The router dedupes meta by property but not links, so the root canonical shipped next to every route's own, and the root og:url leaked the homepage URL into routes that did not override it. Each page route now sets both. The bare /docs page's canonical drops its trailing slash (/docs/ 307-redirects to /docs, and the sitemap lists /docs).
  • Blog index metadata: /blog gets og:url, twitter:title and twitter:description, a Blog JSON-LD block, and a screen-reader-only h1. No markdown alternate link: /blog has no markdown variant (the middleware in src/start.ts only serves .md for the homepage, docs pages, and individual posts). Series pages get og:url and their own twitter:title and twitter:description; the privacy page gets its own og:title, og:description, and og:url.
  • RSS autodiscovery: every page links /blog/rss.xml with rel="alternate" type="application/rss+xml".

Note: per-page generated OG images are the next step and stay in the Linear issue; I made the existing generic image render first. Series pages still have no JSON-LD, pre-existing and out of scope.

To test

Preview deployments sit behind deployment protection, so unfurlers can only be checked on production after merge.

Tested locally against the built server function (pnpm build, then handler.fetch on .vercel/output/functions/__server.func/index.mjs) over /, /blog, /blog/listen-notify, /blog/series/connection-pooling, /docs, /docs/architecture, /privacy:

  • curl -s <page> | grep -c 'rel="canonical"', expect 1 with href equal to the page URL
  • curl -s <page> | grep -o '<meta property="og:image"[^>]*>', expect exactly one tag with content="https://multigres.com/img/og-image.png"; same for twitter:image; og:url equals the canonical; og:title and twitter:title each appear once
  • curl -s /blog, expect an <h1>, one application/ld+json block of type Blog, and twitter:title equal to Blog | Multigres
  • Any page, expect <link rel="alternate" type="application/rss+xml" href="https://multigres.com/blog/rss.xml">
  • After merge: X Card Validator on a blog URL, expect the large image card (X caches cards for about a week, so use the validator rather than an existing tweet)

Linear

  • Part of GROWTH-1208

…iscovery

Social unfurls on X, Slack, and LinkedIn were rendering without an image
because og:image and twitter:image were emitted as the relative path
/img/og-image.png. Every route now uses the absolute defaultOgImage from
src/lib/shared.ts, and blog posts resolve a frontmatter image to an
absolute URL before it reaches og:image and JSON-LD.

The root layout no longer sets canonical or og:url. TanStack Router
dedupes meta by property but not links, so the root canonical shipped
alongside every route's own canonical, and the root og:url leaked the
homepage URL into routes that did not override it. Each page route sets
its own og:url and canonical; the root gains the RSS autodiscovery link.

The blog index gets og:url, twitter:title/description, a Blog JSON-LD
block, and a screen-reader-only h1. Series pages get og:url and their
own twitter:title/description.

Part of GROWTH-1208

Signed-off-by: Pamela Chia <pamelachiamayyee@gmail.com>
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

@pamelachia is attempting to deploy a commit to the Supabase Team on Vercel.

A member of the Team first needs to authorize it.

The bare /docs page built its canonical as /docs/ from an empty splat,
and /docs/ redirects to /docs, so with the root canonical gone the page
would have pointed its only canonical at a redirect. absoluteUrl now
resolves through the URL parser so rootless, protocol-relative, and
mixed-case absolute frontmatter image values all produce a fetchable
og:image instead of a concatenated string. The privacy page gets its
own og:title and og:description alongside the og:url added earlier.

Part of GROWTH-1208

Signed-off-by: Pamela Chia <pamelachiamayyee@gmail.com>
@pamelachia

Copy link
Copy Markdown
Contributor Author

Superseded by #78 (same branch, pushed to origin now that access is sorted).

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