How toAdd dynamic canonical URL to Remix routes

Canonical URLs is the original and official URL of a document, used to tell search engines which one is the real URL.

As an example if you have the URL /list?sort=asc you can set the canonical URL to be /list so Google only index the /list version, since the content of both is most likely the same we want Google to only index one.

This can also be used to ensure that if Google finds URLs like /article?utm_source=newsletter it will not consider it a duplicate of /article.

To set a canonical URL we need to the the <link rel="canonical" href="url" /> tag, but because the href needs to be the full URL and not just the path we need to know the host of the request.

So we will need to use MetaFunction instead of LinksFunction in our Remix's route.

app/routes/article.tsx
import type { LoaderFunctionArgs, MetaDescriptor, MetaFunction } from "@remix-run/cloudflare"; import { json } from "@remix-run/cloudflare"; export const meta: MetaFunction<typeof loader> = ({ data }) => data?.meta ?? []; export async function loader({ request }: LoaderFunctionArgs) { return json({ meta: [ { tagName: "link", rel: "canonical", href: request.url } ] satisfies MetaDescriptor[]; }); }

By doing this we can use the request.url as the canonical URL, but this will use the whole URL, including search params. We can remove them instead.

-      { tagName: "link", rel: "canonical", href: request.url }
+      { tagName: "link", rel: "canonical", href: new URL("/article", request.url).toString() }

With this change, we can use the request.url as base for the protocol and host, but replace the rest with /article.

We can also instance URL and then remove the search params which can be useful to keep route params.

export async function loader({ request }: LoaderFunctionArgs) {
+ let url = new URL(request.url);
+ url.searchParams.forEach((_, key) => url.searchParams.delete(key));
  return json({
    meta: [
+     { tagName: "link", rel: "canonical", href: url.toString() }
    ] satisfies MetaDescriptor[];
  });
}