How to add dynamic canonical links in Remix application

June 26th, 2022

I rewrote my blog using several months ago, but I faced an issue with canonical links when I started improving the SEO of my blog. After some research about this, I came to a perfect solution that lets me have dynamic link elements that absorb route data. Let me show you how I got to it.

What is a canonical URL

If you already know what it is and why you need it, jump to the next section.

Simply put, a canonical URL is used by search engines to determine the origin of the content. Suppose your blog post is available through http://, https:// and/or www. URLs. In that case, it is highly recommended to put a canonical URL that follows some structure, so that search engines will mark other pages as duplicates and give all the credits to the original content.

Another popular reason to use canonical URLs is to syndicate your content. If you publish your post on your blog and third-party platforms(like,, etc.) Google will consider them as duplicate content. It might hurt the ranking of your website but using the canonical URLs you can turn this on its head and get back again all the SEO credits to your website by setting up canonical URLs in third-party platforms. This blog post is not meant to explain everything about canonical URLs, so if you still have questions I highly recommend reading about it here.

The problem with the links in

Most likely, you have a template route like blog.$slug.tsx in your Remix application. I thought I could add canonical URLs using a links function, just like this(assuming I return canonicalUrl in the route loader):

export const links: LinksFunction = ({ data }) => {

  return [{
    rel: 'canonical', href: data.canonicalUrl,

But TypeScript immediately turned me down and didn't accept having an object with data in parameters. Turns out LinksFunction does not have access to data returned by loader and we can't put there any dynamic content.

Solution: use DynamicLinks

So after some research on the internet about adding dynamic link elements to a page in Remix, I came to a solution using DynamicLinks. So, DynamicLinks is not something that comes from the documentation it is rather a utility that can be built using capabilities.

Big thanks to Sergio who implemented it in remix-utils Open-Source library.

How does DynamicLinks utility works

Let me show you the code and walk you through what is happening:

export function DynamicLinks() {
  let links: LinkDescriptor[] = useMatches().flatMap((match) => {
    let fn = match.handle?.dynamicLinks;
    if (typeof fn !== 'function') return [];
    return fn({ data: });

  return (
      { => (
        <link {} key={link.integrity || JSON.stringify(link)} />
  • First of all, we get the all matched routes using the useMatches utility hook
  • We look for our pre-defined dynamicLinks function in exported handle constant of each matched route and call it giving the route data as a parameter(if the function is defined by the route)
  • After we collect links from all the matched routes, we simply render them

We can put this component in the root of the Remix app and it will work for all the routes that define dynamicLinks function in exported handle variable.

How to use DynamicLinks in Remix app

The usage of the utility is very simple. I define dynamicLinks function in my blog.$slug.tsx module and export it within handle Remix-defined constant:

const dynamicLinks: DynamicLinksFunction<LoaderData> = ({ data }) => {
  return [
      rel: 'canonical', href: data.canonicalUrl,

export const handle = {

Note that this code assumes that I return canonicalUrl from my loader function.

And then I just need to render DynamicLinks component in the root of the Remix application:

import { DynamicLinks } from "remix-utils";

export default function App() {

 return (
    <html lang="en">
        {/** your head tags */}
        <DynamicLinks />
        {/** body tags */}
        <Outlet />
        <Scripts />

This results in having a dynamic canonical URLs in all of my blog posts(both client and server-side). E.g.:

<link rel="canonical" href=""/>

That's it! I was upset in the beginning that LinksFunction does not support loader data(for sure, for some good reason), but I liked how Remix gives the flexibility to implement this kind of utility. There are more examples in the documentation about the useMatches utility hook, like implementing breadcrumbs for all the nested components.

Thank you for reading this post. If you liked it make sure to share it with others.

Get updates about new posts

I committed to share more this year, so if you don’t want to miss anything, please subscribe to my newsletter. I hope you will enjoy the content!