Den manglende linken

Alle rammeverk har sine særegenheter – Next.js er ikke et unntak. Jeg kom over én slik ting da jeg prøvde å debugge hvorfor jeg fikk en 404-feil da jeg prøvde å dele en artikkel på Facebook. Dette er historien om hva som skjedde.

Første akt: Misvisende metadata

Når man deler en nettside på Facebook, henter Facebook noen metadata fra nettsiden. Blant disse er en meta-tagg som heter og:url. Den skal inneholde en full URL til siden du deler. Jeg hadde denne meta-taggen, men den hadde en feil. URL-en var https://djupvik.digital/blog/2024/11/hallo-verden, men den skulle ha vært https://djupvik.digital/nb/blog/2024/11/hallo-verden. Next.js har støtte for flerspråklige nettsteder, og alle norske sider ligger under språkprefikset nb. Noe var galt i koden min (igjen!).

Før jeg fortsetter vil jeg bare gjøre oppmerksom på at nettstedet mitt bruker den gamle «pages-ruteren» i Next.js, så koden vil være annerledes enn hvis du bruker den nyere «app-ruteren».

Det var ikke vanskelig å finne kilden til feilen. Metadata håndterer jeg i en egen React-komponent. Her er komponenten (redusert til et minimum av relevant kode, og delvis endret for å gjøre koden lettere å forstå):

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

Problemet ligger i router.asPath. Og riktig nok, dette fant jeg i dokumentasjonen: «basePath og locale er ikke inkludert» (min oversettelse). Så router.asPath gir ikke den riktige banen til en språkspesifikk side.

Så hvorfor ikke bare legge på locale manuelt? Slik:

const fullURL = new URL(`/${router.locale}/${router.asPath}`, server)

Joda, og så må man sjekke at du ikke legger på router.defaultLocale, for den URL-en har ikke router.locale foran seg. Alle elementene er jo der. Poenget er: hvorfor gjør Next.js det så vanskelig? Hvorfor ikke bare gi meg den riktige URL-en?

Andre akt: Jakten på URL-en

I stedet for å følge det fornuftige rådet i forrige avsnitt, falt jeg ned i ett av mine sedvanlige kaninhull. Next.js må da ha en eller annen funksjon for å returnere URL-en for den gjeldende siden, tenkte jeg. Jeg googlet etter en løsning.

Jeg kom over en diskusjon på GitHub hvor det henvises til en intern funksjon i Next.js som heter resolveHref. Den så ut til å være løsningen på problemet. Men gjett hva som skjedde da jeg prøvde å bruke den? Akkurat samme problem som før – locale er ikke inkludert.

Hva nå? Jeg leste noen flere tips. Noen anbefalte å lage en middleware-funksjon. Det finnes til og med en npm-pakke. Det er bare det at nesten alle rutene mine blir statisk generert, og middleware gjør at jeg må server-rendere alle rutene.

Det var en slags trøst i at problemet er ett som flere har hatt. Hvorfor er det så vanskelig å få en URL fra Next.js? Jeg kom over en lang forklaring. Men noen løsning var ikke i sikte.

Da var det ett eller annet bak i hjernen min som begynte å røre på seg. Hva hvis …?

Tredje akt: Link redder dagen

Her kommer løsningen som jeg kom frem til: Next.js har en link-komponent. Det er jo bare å wrappe Metadata-komponenten i en Link. Kunne det virkelig være så enkelt? Jeg fant frem dokumentasjonen til <Link>. Der var det et avsnitt som ga meg svaret.

Jeg skyndte meg å implementere endringene, og testet. Suksess! Det var virkelig så enkelt!

Her kommer koden (redusert til et minimum). Først har vi komponenten for siden:

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

Og så har vi koden for Metadata-komponenten:

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

Som du ser trengs bare et objekt med de dynamiske delene av URL-en som hrefLink. Metadata får den ferdige href-en som en prop.

Meta-tag fremhevet i Vis kilde
Skjermbilde av meta-taggen

Teppefall

Hva er moralen i dette lille hverdagsdramaet? Vanskelig å si. Jeg kastet bort mye tid på dette tullet. Jeg kunne spart mye tid ved å bare lage URL-en selv. På den annen side var det litt gøy å finne en sånn merkelig workaround. Så her legger jeg løsningen frem, slik at noen andre som har samme problemet, forhåpentligvis kan spare litt tid.