Site Head and Footer Snippets Without a Redeploy
Snippet changes used to mean an edit, a commit, and a deploy wait. A whitelisted settings endpoint turns that into one PUT request.
A while back I had to swap one Google tag for another on my own site. One line of JavaScript. The routine: open layout.tsx, paste, commit, push, stare at the deploy bar for a few minutes, hope nothing else in the bundle regressed. I've done this a dozen times this year and never questioned it.
Commit 62a2d4b killed the routine. The same change is now one PUT request to a settings endpoint, and the site picks it up on the next render. No git, no build, no deploy.
The assumption that held for too long
My mental model for years: third-party script goes in with next/script, default strategy, done. afterInteractive loads after hydration starts, works from any page or layout, and for analytics that's genuinely fine [1]. Most snippets don't care when they run.
First crack: a Google Search Console verification tag. GSC wants proof you own the domain, and a verified owner gets the highest permission level on the property [3]. The tag sits in the head, and that kind of tag is expected to exist before the framework's own code runs. afterInteractive by definition fires after hydration starts [1], later than I assumed.
Next.js has a strategy for exactly this: beforeInteractive. It loads before any Next.js code and before hydration, it must be placed in the root layout, and Next always injects it into the HTML head [1]. The docs also note it doesn't block hydration; it just executes earlier. So the rule I landed on: head tags that crawlers or verifiers read go beforeInteractive, everything else stays on the default.
Second thing I had wrong: hand-pasting the verification meta tag like it was 2019. Static metadata that doesn't depend on request info belongs in the metadata object, not generateMetadata [2], and metadata.verification.google renders the google-site-verification tag for you [2]. That mattered because the token itself became just another whitelisted setting in the API.
The settings module
The FastAPI backend got a settings module built DDD-style, with a hard whitelist of three keys: google_site_verification, head_snippet, footer_snippet. Anything outside the whitelist gets rejected. The API won't store arbitrary HTML keys someone smuggles in.
Two endpoints. A public GET /settings/site with no auth, because the rendered site needs to read these values. Admin PUT and DELETE behind an API key header. All of it backed by Redis with tag-based caching, and invalidation on writes so stale snippets never outlive the request that changed them [4]. Seven tests run against in-memory sqlite. The whole commit was 697 insertions across 23 files [5].
On the frontend, two tiny components in the root layout. The head one:
import Script from "next/script";
import { fetchSiteSettings } from "@/services/settings-service";
export default async function SiteHeadSnippet() {
const settings = await fetchSiteSettings();
if (!settings.head_snippet) return null;
return (
<Script
id="site-head-snippet"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{ __html: settings.head_snippet }}
/>
);
}
The footer twin is the same shape with strategy="afterInteractive". That one really can run late; nobody verifies a site through a footer script.
An honest trade-off: dangerouslySetInnerHTML fed from a database is exactly what the name warns about. The whitelist is the mitigation. Only admin requests with the API key can write these keys, and the keys are fixed. If someone gets your API key, they own your head. Treat it like a deploy credential, because functionally it is one now.
Why this holds up
The shift isn't the components. It's that a deploy became a runtime operation. Snippet changes were never really code changes; they were config wearing a code costume, and I paid the deploy tax every time because storing HTML in a database felt wrong.
It still feels slightly wrong, and I've made peace with it. Cache invalidation on write is what makes it sane: Redis discards the stale entries the moment the setting changes [4], so the next render rebuilds with fresh content instead of serving yesterday's tag. Without that, the design trades a deploy delay for a staleness lottery, and I'd rather have the deploy.
Quieter win: google_site_verification lives in the same settings object, so when GSC asks for re-verification I don't touch the frontend at all. Set the value, metadata does the rest [2]. Next time a tag needs rotating, the diff is zero lines and one HTTP call.
Sources I used
[1] https://nextjs.org/docs/app/api-reference/components/script
[2] https://nextjs.org/docs/app/api-reference/functions/generate-metadata
[3] https://support.google.com/webmasters/answer/9008080?hl=en
[4] https://redis.io/glossary/cache-invalidation
[5] https://github.com/didiet86/my-apps/commit/62a2d4b1632dbd54d0784763695a1076e7da03b4