The flags had titles that repeated the button text. Also mark each
language name with its own lang so it's pronounced correctly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Expose its state with aria-expanded, close it on outside clicks,
Escape and when focus leaves, restore the focus outline, and move the
inline margins into CSS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build the new image before replacing the running container instead of
stopping the site first, prune old images, prevent overlapping deploys,
and drop the unused pull-requests permission.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Install with npm ci, ship only the standalone server and its traced
dependencies, and run as a non-root user. Shrinks the image from
1.78 GB to 438 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Server components read their translations on the server, but every
namespace was serialized into each page for the client provider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jsxBracketSameLine was replaced by bracketSameLine in Prettier 3.
Ignore tsconfig.json, since Next rewrites it in its own format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Icons were set in both the layout metadata and every page's metadata,
and theme-color was output twice. Inherit icons from the layout and
export the viewport from the layout instead of from each page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The config was split between lib/analyticsConfig and a client component
that repeated the production check and default URL, and checked window
during render. Umami's data-domains already restricts tracking to our
domains.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event dates are stored as Oslo wall-clock strings, but were parsed in
the server's local timezone, so times were only correct because the
container sets TZ=Europe/Oslo. Move date parsing and formatting into
lib/dates, interpret those strings as Oslo time, and also accept ISO
timestamps in case the Studio writes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Locales were listed separately in the middleware, the request config
and several type casts. Use defineRouting and createNavigation, type
the Locale globally, and let the language selector use the locale-aware
router so it keeps the query string and hash.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Node 20 is end-of-life; use the current LTS in the Docker image
- Major updates: @sanity/client 8, @sanity/image-url 2,
@portabletext/react 8, eslint-config-next 16
- Unpin React and update to 19.3
- ESLint stays on 9 and TypeScript on 6.0, since eslint-config-next's
plugins and typescript-eslint don't support newer majors yet
- Use the native flat config from eslint-config-next 16
- Use useSyncExternalStore for WebGL detection to satisfy the new
react-hooks lint rule
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blog links come from the CMS, so a javascript: URL would run code when
clicked. Allow only http(s), mailto and relative links, and render
anything else as plain text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The items were buttons calling window.open() without noopener, which
let the opened page access window.opener. Links also support
middle-click and show the URL on hover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The secret was passed in the query string, which ends up in access logs
and doesn't protect the request body. Verify the signed header with
@sanity/webhook instead, and return 400 for invalid JSON.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filter and limit in GROQ instead of fetching every event and filtering
in JavaScript. Event dates are stored as Oslo local time strings, so
compare them against the current Oslo time in the same format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event cards crashed on a missing description, and the event metadata
included "null" for missing fields. Truncate previews by grapheme so
emoji aren't split, and show descriptions of exactly 70 characters in
full.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a .dockerignore, COPY . . baked the .env file (including the
webhook secret) into the image and overwrote the container's
node_modules with the host's. Pass public build-time values as build
args and provide the rest at runtime through env_file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getLocale() read request headers, which made every route dynamic and the
page-level revalidate a no-op. Make [locale]/layout.tsx the root layout
so the locale can be read through next/root-params, set <html lang>, add
generateStaticParams for locales and slugs, and pass the locale
explicitly to metadata translations.
The middleware now matches all non-file paths so URLs without a locale
are redirected instead of hitting the root layout's notFound().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "max" profile serves stale content once more while revalidating, so
the first visit after publishing showed the old version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>