Custom domain & canonical
When your blog renders on your domain, you have to decide one thing deliberately: what goes in <link rel="canonical">. Get it wrong and search engines either deduplicate against the wrong URL or split ranking signal. This guide is about owning that decision.
The two URLs in play
Section titled “The two URLs in play”Each article carries canonicalUrl — an absolute URL, or null if the site has no URL set. It is the canonical Ghost Writr believes in, and it may point somewhere other than where you’re rendering:
- It may point to your blog (the common case — you set your site URL in Ghost Writr).
- It may point elsewhere — for syndicated or adopted content, the canonical can intentionally credit another origin.
So canonicalUrl is a value to respect, not blindly stamp with your own page URL. The rule:
- You own the canonical home → emit
https://your-domain.com/blog/{slug}(the page’s own URL). - The article points elsewhere on purpose → keep
article.canonicalUrlso you don’t claim content that canonically belongs to another origin.
Per framework
Section titled “Per framework”Set alternates.canonical in generateMetadata. Build the URL from your own domain and the slug — that’s the “I own this” choice:
import type { Metadata } from "next";import { gw } from "@/lib/ghostwritr";
const SITE = "https://your-domain.com";
export async function generateMetadata({ params,}: { params: Promise<{ slug: string }>;}): Promise<Metadata> { const { slug } = await params; const article = await gw.getArticle(slug); if (!article) return {};
// Own the canonical: your domain + slug. Swap to article.canonicalUrl // only if you want to honor a deliberate cross-origin canonical. const canonical = `${SITE}/blog/${article.slug}`; return { title: article.seoTitle, description: article.seoDescription ?? undefined, alternates: { canonical }, };}articleMeta takes a url option — pass your page’s own absolute URL and it becomes both <link rel="canonical"> and og:url. Omit it and articleMeta falls back to article.canonicalUrl:
export const meta: Route.MetaFunction = ({ data, location }) => data ? articleMeta(data.article, { siteName: "Acme", url: `https://your-domain.com${location.pathname}`, // own the canonical }) : [];To honor the article’s own canonical instead, simply drop the url option.
The loader puts the article’s canonical on entry.data.canonicalUrl. In your layout, choose between that value and your own page URL:
---import { getCollection } from "astro:content";// ...resolve `entry` for the current slug...const own = new URL(Astro.url.pathname, Astro.site).href; // your domain + pathconst canonical = own; // own it — or use entry.data.canonicalUrl to honor the feed's---<link rel="canonical" href={canonical} />Astro.site comes from your astro.config site option, so set that to your blog’s domain.
What to reach for next
Section titled “What to reach for next”- SEO end to end — every field, not just the canonical. See SEO & structured data.
- The article shape — where
canonicalUrlsits and whatnullmeans. See The article shape. - The concept — canonical and JSON-LD reasoning. See SEO and JSON-LD.