The Missing Link
All frameworks have their quirks – Next.js is no exception. I came across one such when I tried to debug why I was getting a 404 error trying to share an article on Facebook. This is the story about what happened.
Act one: Misleading metadata
When you share a website on Facebook, it grabs some metadata from the web page. Among those are a meta tag called og:url
. It’s supposed to have the full URL of the page you’re sharing. I had this meta tag, but there was an error. The URL was https://djupvik.digital/blog/2024/11/hallo-verden
, but it should have been https://djupvik.digital/nb/blog/2024/11/hallo-verden
. Next.js supports multilingual websites, and all the Norwegian pages are unde the language prefix nb
. Something was wrong in my code (again!).
Before I continue I’d like to clarify that I’m using the older “Pages Router” in Next.js, so the code will be different from using the newer “App Router”.
It wasn’t difficult to find the error. I’m handling metadata in a bespoke React component. Here’s the component (reduced to a minimum of relevant code, and slightly changed to make the code easier to understand):
import Head from 'next/head'
import { useRouter } from 'next/router'
interface MetadataProps {
}
const server = 'https://example.com'
function Metadata(props: MetadataProps) {
const router = useRouter()
const fullURL = new URL(router.asPath, server)
return (
<Head>
<meta content={fullURL.href} key="og:url" property="og:url" />
</Head>
)
}
export default Metadata
The problem lies in router.asPath
. And sure enough, I found this in the documentation: ”basePath
and locale
are not included”. So router.asPath
does not give the right path to a language specific page.
Why not just add locale
manually? Like this:
const fullURL = new URL(`/${router.locale}/${router.asPath}`, server)
Sure, and the you just have to check if you’re using router.defaultLocale
, because that URL doesn’t have router.locale
prepended. All the elements are there. The point is: Why does Next.js make it so difficult? Why not just give me the right URL?
Act two: The hunt for the URL
Instead of following the sensible advice in the last paragraph, I went down one of my usual rabbit holes. Next.js surely must have a function for returning the URL of the current page, I thought. I googled for a solution.
I came across a discussion on GitHub where it’s referred to an internal function in Next.js called resolveHref
. It looked like the solution to the problem. But guess what happened when I tried to use it? The exact problem as before – locale
is not included.
Now what? I read a few more tips. Some recommended making a middleware function. There’s even an npm package. It’s just that almost all my routes are statically generated, and middleware means I have to server render all the routes.
There was some comfort to be found in the fact that this is a problem that others have had too. Why is it so difficult to get a URL from Next.js? I came across a long explanation, but there was no solution in sight.
The suddenly something in the back of my mind started moving. What if…?
Act three: Link saves the day
Here’s the solution I arrived at: Next.js has a link component. I could just wrap the Metadata component in a Link. Could it really be this easy? I found the documentation for <Link>
. It had a paragraph that provided the answer.
I hurried implementing the changes, and tested them. Success! It was really that simple!
Here’s the code (reduced to a minimum). First we have the component for the page:
import type { GetStaticProps } from 'next'
import Link from 'next/link'
import Metadata from '../../../../src/scripts/components/metadata'
interface Params {
month: string
slug: string
year: string
}
interface PostProps {
params: Params
}
function Post({ params }: PostProps) {
const href = { query: params }
return (
<Link href={href} legacyBehavior passHref>
<Metadata />
</Link>
)
}
export const getStaticProps: GetStaticProps<{}, Params> = async (context) => {
const {
params = { month: '', slug: '', year: '' },
} = context
return { props: { params } }
}
export default Post
And then we have the Metadata component:
import Head from 'next/head'
import { forwardRef, type ForwardRefRenderFunction } from 'react'
interface MetadataProps {
href?: string
}
const server = 'https://example.com'
const Metadata: ForwardRefRenderFunction<HTMLMetaElement, MetadataProps> = (
props,
ref,
) => {
const { href } = props
const fullURL = new URL(href || '/', server)
return (
<Head>
<meta content={fullURL.href} key="og:url" property="og:url" ref={ref} />
</Head>
)
}
const ForwardedMetadata = forwardRef(Metadata)
export default ForwardedMetadata
As you can see, you only need an object with the dynamic parts of the URL as href
on Link
. Metadata
gets the computed href
as a prop.
Curtains
What is the moral of this little everyday drama? Hard to tell. I threw away a lot of time on this nonsense. I could have saved a lot of time just doing the URL myself. On the other hand it was kinda fun to find such a weird workaround. Here I provide the solution, so that others who have the same problem hopefully will save a little time.