From Kent C. Doddsâ article about why he wonât be using Next.js:
One of the primary differences between enzyme and Testing Library is that while enzyme gave you a wrapper with a bunch of (overly) helpful (dangerous) utilities for interacting with rendered elements, Testing Library gave you the elements themselves. To boil that down to a principle, I would say that instead of wrapping the platform APIs, Testing Library exposed the platform APIs.
Iâve been recently working in a Next.js app and a lot of Kentâs critiques have resonated with my own experience, particularly this insight about how some APIs wrap platform ones rather than exposing them.
For example, one thing I struggled with as a n00b to Next is putting metadata in an HTML document. If you want a <meta>
tag in your HTML, Next has a bespoke (typed) API dedicated to it.
I understand why that is the case, given how Next works as an app/routing framework which dynamically updates document metadata as you move from page to page. Lots of front-end frameworks have similar APIs.
However, I prefer writing code as close as possible to how it will be run, which means staying as close as possible to platform APIs.
Why? For one, standardized APIs make it easy to shift from one tool to another while remaining productive. If I switch from tool A to tool B, itâd be a pain to relearn that <div>
is written as <divv>
.
Additionally, you donât solely write code. You also run it and debug it. When I open my webpage and thereâs a 1:1 correspondence between the <meta>
tags I see in the devtools and the <meta>
tags I see in my code, I can move quickly in debugging issues and trusting in the correctness of my code.
In other words, the closer the code thatâs written is to the code thatâs run, the faster I can move with trust and confidence. However, when I require documentation as an intermediary between what I see in the devtools and what I see in my code, I move slower and with less trust that Iâve both understood and implemented correctly what is documented.
With Next, what I write compiles to HTML which is what the browser runs. With plain HTML, what I write is what the browser runs. Itâs weird to say writing plain HTML is âcloser to the metalâ but here we are ha!
That said, again, I realize why these kinds of APIs exist in client-side app/routing frameworks. But with Next in particular, Iâve encountered a lot of friction taking my base understanding of HTML APIs and translating them to Nextâs APIs. Allow me a specific example.
The basic premise of Nextâs metadata API starts with the idea that, in order to get some <meta>
tags, you use the key/value pairs of a JS object to generate the name
and content
values of a <meta>
tag. For example:
export const metadata = {
generator: 'Next.js',
applicationName: 'Next.js',
referrer: 'origin-when-cross-origin',
}
Will result in:
<meta name="generator" content="Next.js" />
<meta name="application-name" content="Next.js" />
<meta name="referrer" content="origin-when-cross-origin" />
Simple enough, right? camelCased keywords in JavaScript translate to their hyphenated counterparts, thatâs all pretty standard web API stuff.
But what about when you have a <meta>
tag that doesnât conform to this simple one-key-to-one-value mapping? For example, letâs say you want the keywords
meta tag which can have multiple values (a comma-delimited list of words):
<meta name="keywords" content="Next.js,React,JavaScript" />
Whatâs the API for that? Well, given the key/value JS object pattern of the previous examples, you might think something like this:
export const metadata = {
keywords: 'Next.js,React,JavaScript'
}
Minus a few special cases, thatâs how Remix does it. But not in Next. According to the docs, itâs this:
export const metadata = {
keywords: ['Next.js', 'React', 'JavaScript'],
}
âAh ok, so itâs not just key/value pairing where value
is a string. It can be a more complex data type. I guess that makes sense.â I say to myself.
So what about other meta tags, like the ones whose content
is a list of key/value pairs? For example, this tag:
<meta
name="format-detection"
content="telephone=no, address=no, email=no"
/>
How would you do that with a JS object? Initially you might think:
export const metadata = {
formatDetection: 'telephone=no, address=no, email=no'
}
But after what we saw with keywords
, you might think:
export const metadata = {
formatDetection: ['telephone=no', 'address=no', 'email=no']
}
But this one is yet another data type. In this case, content
is now expressed as a nested object with more key/value pairs:
export const metadata = {
formatDetection: {
email: false,
address: false,
telephone: false,
},
}
To round this out, letâs look at one more example under the âBasic fieldsâ section of the docs.
export const metadata = {
authors: [
{ name: 'Seb' },
{ name: 'Josh', url: 'https://nextjs.org' }
],
}
This configuration will produce <meta>
tags and a link
tag.
<meta name="author" content="Seb" />
<meta name="author" content="Josh" />
<link rel="author" href="https://nextjs.org" />
âAh oh, so the metadata
keyword export isnât solely for creating <meta>
tags. Itâll also produce <link>
tags. Got it.â I tell myself.
So, by solely looking at the âBasicsâ part of the docs, Iâve come to realize that to produce <meta>
tags in my HTML I should use the metadata
keyword export which is an object of key/value pairs where value
can be a string, an array, an object, or even an array of objects! All of which will produce <meta>
tags or <link>
tags.
Ok, I think I got it.
Not So Fast: A Detour to Viewport
While you might think of the viewport meta tags as part of the metadata API, theyâre not. Or rather, they were but got deprecated in Next 14.
Deprecated: The viewport option in metadata is deprecated as of Next.js 14. Please use the viewport configuration instead.
[insert joke here about how the <meta>
tag in HTML is never gonna give you up, never gonna let you down, never gonna deprecate and desert you]
Ok so viewport has its own configuration API. How does it work?
Let's say I want a viewport tag:
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
Whatâs the code for that? Given our knowledge of the metadata API, maybe we can guess it.
Since it gets its own named export, viewport
, I assume the content
part of the tag will represent the key/value pairs of the object?
And yes, thatâs about right. Here's the code to get that tag:
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
}
Ok, I guess that kinda makes sense. false
= no
and all, but I see whatâs going on.
But the viewport
export also handles other tags, not just <meta name="viewport">
. Theme color is also under there. You want this tag?
<meta name="theme-color" content="black" />
You mightâve thought itâs this:
export const metadata = { themeColor: 'black' }`
But according to the docs it's part of the viewport
named export:
export const viewport = { themeColor: 'black' }
And what if you want multiple theme color meta tags?
<meta
name="theme-color"
media="(prefers-color-scheme: light)"
content="cyan"
/>
<meta
name="theme-color"
media="(prefers-color-scheme: dark)"
content="black"
/>
Well thatâs the viewport
named export but instead of a string you give it an array of objects:
export const viewport = {
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'cyan' },
{ media: '(prefers-color-scheme: dark)', color: 'black' },
],
}
Ok, I guess this all kind of makes sense â in its own self-consistent way, but not necessarily in the context of the broader web platform APIsâŠ
Ok so, given everything covered above, letâs play a little game. I give you some HTML and you see if you can guess its corresponding API in Next. Ready?
<link
rel="canonical"
href="https://acme.com"
/>
<link
rel="alternate"
hreflang="en-US"
href="https://acme.com/en-US"
/>
<link
rel="alternate"
hreflang="de-DE"
href="https://acme.com/de-DE"
/>
<meta
property="og:image"
content="https://acme.com/og-image.png"
/>
Go ahead, Iâll give you a second. See if you can guess it...
Have you tried? Iâll keep waiting...
Got it?
Ok, hereâs the answer:
export const metadata = {
metadataBase: new URL('https://acme.com'),
alternates: {
canonical: '/',
languages: {
'en-US': '/en-US',
'de-DE': '/de-DE',
},
},
openGraph: {
images: '/og-image.png',
},
}
Thatâs it. Thatâs what will produce the HTML snippet I gave you. Apparently thereâs a whole âconvenienceâ API for prefixing metadata fields with fully qualified URLs.
Youâve heard of CSS-in-JS? Well this is HTML-in-JS. If you wish every HTML API was just a (typed) JavaScript API, this would be right up your alley. No more remembering how to do something in HTML. Thereâs a JS API for that.
And again, I get it. Given the goals of Next as a framework, I understand why this exists. But thereâs definitely a learning curve thatâs feels divergent to the HTML pillar of the web.
Contrast that, for one moment, with something like this which (if you know the HTML APIs) requires no referencing docs:
const baseUrl = 'https://acme.com';
export const head = `
<link
rel="canonical"
href="${baseUrl}"
/>
<link
rel="alternate"
hreflang="en-US"
href="${baseUrl}/en-US"
/>
<link
rel="alternate"
hreflang="de-DE"
href="${baseUrl}/de-DE"
/>
<meta
property="og:image"
content="${baseUrl}/og-image.png"
/>
`;
I know, I know. Thereâs tradeoffs here. But I think what I'm trying to get at is what I expressed earlier: thereâs a clear, immediate correspondence in this case between the code I write and what the browser runs. Plus this knowledge is transferable. This is why, to Kentâs point, I prefer exposed platform APIs over wrapped ones.
Conclusion
I only briefly covered parts of Nextâs metadata API. If you look closer at the docs, youâll see APIs for generating <meta>
tags related to open graph, robots, icons, theme color, manifest, twitter, viewport, verification, apple web app, alternates, app links, archives, assets, bookmarks, category, and more.
Plus thereâs all the stuff that you can use in âvanillaâ HTML but thatâs unsupported in the metadata API in Next.
This whole post might seem like an attempt to crap on Next. Itâs not. As Kent states in his original article:
Your tool choice matters much less than your skill at using the tool to accomplish your desired outcome
I agree.
But I am trying to work through articulating why I prefer tools that expose underlying platform APIs over wrapping them in their own bespoke permutations.
It reminds me of this note I took from an article from the folks building HTMX:
Whenever a problem can be solved by native HTML elements, the longevity of the code improves tremendously as a result. This is a much less alienating way to learn web development, because the bulk of your knowledge will remain relevant as long as HTML does.
Well said.
Reply