
Headless CMS in Hungary: Sanity vs Strapi vs Contentful in 2026
A deep dive into three headless CMS options: Sanity, Strapi and Contentful. Pricing, real-time preview, localisation and five Hungarian-market use cases.
25 concrete checks a Next.js 16 project must pass before go-live, covering security, performance, SEO, accessibility and monitoring in one list.

Pre-launch checklist
CSP, HSTS, X-Frame, Referrer-Policy. Rate-limit on critical routes and never commit .env to git.
SSG or ISR wherever possible, next/image with sizes, bundle-analyzer in CI. Lighthouse 90+ on mobile.
Metadata, JSON-LD, sitemap and robots. hreflang on multi-locale routes. WCAG AA contrast and full keyboard nav.
Sentry with source-map upload, Vercel Analytics or Plausible, plus uptime monitoring wired into Slack.
Next.js 16 has been stable since early 2026, with Turbopack as the default bundler and a set of new App Router features. Still, a production-ready deployment comes with plenty of small details that are easy to miss. This checklist covers 25 concrete items, grouped by category. The points reflect the Next.js 16.2+ state as of Q1 2026; newer releases may change some of this, so it's worth cross-checking the official docs as well.
The 25 points aren't exhaustive — a given project may need more (GDPR, analytics segmentation, A/B testing infrastructure). But with this checklist, launch day won't turn into a "we should have done this too" panic.
Before diving into the checklist, here are a few Next.js 16 changes that affect production deployments:
cookies(), headers(), params and searchParams are now all async. Upgrading an existing codebase requires rewriting every call site.use() hook, useFormStatus, useActionState.'use cache' directive for granular caching.Note: During the upgrade,
npx @next/codemod upgradehandles most breaking changes automatically. Manual review is still needed, though — especially for custom middleware anduseSearchParamshooks.
CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. A minimal setup:
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://va.vercel-scripts.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
].join('; '),
},
],
},
];
},
};
export default nextConfig;
For a CSP nonce solution with dynamic inline scripts: use next/script, or generate a nonce in the middleware.
Covered by a .env* pattern in .gitignore. Only .env.example lives in the repo, as a template.
# .gitignore
.env
.env.local
.env.*.local
.env.production
# .env.example
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://user:pass@host:5432/db
OPENAI_API_KEY=sk-...
As a second layer of secret-leak protection: run git-secrets or gitleaks in CI.
Next.js 15+ ships a built-in Origin header check. Don't disable it manually — Server Actions automatically validate that the request originated from your own domain.
// app/actions/contact.ts
'use server';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
message: z.string().min(10).max(2000),
});
export async function submitContact(formData: FormData) {
const parsed = schema.safeParse({
email: formData.get('email'),
message: formData.get('message'),
});
if (!parsed.success) {
return { error: 'Invalid input', issues: parsed.error.issues };
}
// Server-side action — CSRF-protected by default
await db.contacts.insert(parsed.data);
return { success: true };
}
Vercel KV / Upstash Redis plus @upstash/ratelimit. Applies to form submissions, login, and API calls.
// middleware.ts
import { NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '60 s'),
analytics: true,
});
export async function middleware(request) {
if (request.nextUrl.pathname.startsWith('/api/')) {
const ip = request.ip ?? '127.0.0.1';
const { success, limit, remaining } = await ratelimit.limit(ip);
if (!success) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
},
});
}
}
return NextResponse.next();
}
</ characters in JSON-LDInjecting JSON-LD into a script tag can create an XSS risk:
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(schema).replace(/</g, '\\u003c'),
}}
/>
The </ sequence must be escaped, because a </script> substring inside the payload can break out of the script tag.
npm audit should come back clean (0 vulnerabilities). If a Next.js dependency can't be fixed directly, force it with npm overrides.
{
"overrides": {
"postcss": "^8.4.31"
}
}
Plus, in CI:
# .github/workflows/security.yml
- name: Dependency audit
run: npm audit --audit-level=high
- name: License check
run: npx license-checker --failOn 'GPL-3.0'
generateStaticParams for every dynamic route. ISR (revalidate) when content changes but doesn't need to be real-time. RSC by default; client components only where actually needed.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export const revalidate = 3600; // 1 hour ISR
export default async function BlogPost({ params }) {
const { slug } = await params;
const post = await getPostBySlug(slug);
return <Article post={post} />;
}
Use it for every raster image. Set the placeholder="blur" and sizes props.
import Image from 'next/image';
<Image
src="/blog/images/post-slug/hero.jpg"
alt="Descriptive alt text"
width={1200}
height={630}
sizes="(max-width: 768px) 100vw, 1200px"
priority={isAboveFold}
placeholder="blur"
blurDataURL="data:image/svg+xml;base64,..."
/>
The priority prop goes on above-the-fold images — it forces eager loading.
import { Inter, Geist_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin', 'latin-ext'], // Hungarian characters
display: 'swap',
variable: '--font-sans',
});
const geistMono = Geist_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-mono',
});
subsets loads only the characters you actually need. For Hungarian, latin-ext is mandatory (ű, ő, etc.). The self-hosting option ties font loading to build time.
The @next/bundle-analyzer plugin. Run a regression check in CI: alert if the bundle grows by more than 20%.
// next.config.ts
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default withBundleAnalyzer({...});
ANALYZE=true npm run build
# Opens the bundle tree in the browser
CI integration example with size-limit:
{
"size-limit": [
{
"path": ".next/static/chunks/*.js",
"limit": "180 KB"
}
]
}
The App Router splits code by route by default. Avoid the 'use client' directive at the root — scope it narrowly to individual components.
// page.tsx — Server Component (default)
import ClientCounter from './client-counter';
export default async function Page() {
const data = await fetchServerData();
return (
<div>
<h1>{data.title}</h1>
{/* Only this component becomes client-side */}
<ClientCounter initial={data.count} />
</div>
);
}
// client-counter.tsx — Client Component
'use client';
import { useState } from 'react';
export default function ClientCounter({ initial }) {
const [count, setCount] = useState(initial);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
loading="lazy" is the default on next/image. Set priority explicitly for above-the-fold images. Every other image below the fold doesn't load until it approaches the viewport.
Cache-Control: public, max-age=31536000, immutable)cache: 'force-cache' or 'no-store'// Server Component
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // 1 hour ISR
});
return res.json();
}
In Next.js 16, the 'use cache' directive gives even more granular control:
async function getProductList() {
'use cache';
cacheLife('hours');
cacheTag('products');
return db.products.findAll();
}
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Page title — Brand',
description: 'Page description ~150 char',
openGraph: {
title: 'OG title',
description: 'OG description',
images: ['/og-image.jpg'],
locale: 'hu_HU',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Twitter title',
description: 'Twitter description',
images: ['/twitter-image.jpg'],
},
alternates: {
canonical: 'https://example.com/page',
languages: {
'hu-HU': 'https://example.com/hu/page',
'en-US': 'https://example.com/en/page',
},
},
};
Dynamic metadata:
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.image] },
};
}
src/app/sitemap.ts, using Next.js's native support. Include every static and dynamic route.
// app/sitemap.ts
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://example.com';
const posts = await getAllPosts();
const blogUrls = posts.map((p) => ({
url: `${baseUrl}/blog/${p.slug}`,
lastModified: p.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
}));
return [
{ url: baseUrl, priority: 1.0 },
{ url: `${baseUrl}/blog`, priority: 0.8 },
...blogUrls,
];
}
As src/app/robots.ts. On dev/staging environments, set Disallow: / for every user agent.
// app/robots.ts
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const isProduction = process.env.NEXT_PUBLIC_ENV === 'production';
if (!isProduction) {
return {
rules: { userAgent: '*', disallow: '/' },
};
}
return {
rules: [
{ userAgent: '*', allow: '/', disallow: ['/api/', '/admin/'] },
],
sitemap: 'https://example.com/sitemap.xml',
};
}
Add Organization, BlogPosting, FAQPage or Product schema as relevant. Validate with the Schema Markup Validator.
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
datePublished: post.date,
author: {
'@type': 'Organization',
name: 'Corevanix',
},
image: post.coverImage,
};
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(schema).replace(/</g, '\\u003c'),
}}
/>
Set alternates.languages in the metadata on every multi-locale page. Tested with Google Search Console.
Every interactive element is reachable by keyboard. The focus-visible ring is visible (focus-visible:ring-2).
<button
className="rounded bg-accent px-4 py-2 focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-accent-primary
focus-visible:ring-offset-2"
>
Click me
</button>
aria-label on every icon button, aria-labelledby on sections, aria-expanded on accordions.
<button aria-label="Close" onClick={onClose}>
<X aria-hidden="true" />
</button>
<nav aria-label="Main navigation">
<ul>...</ul>
</nav>
<button aria-expanded={isOpen} aria-controls="menu">
Menu
</button>
Text vs. background: at least 4.5:1, large text 3:1. Lighthouse audits this automatically; also check manually with a tool like the WebAIM Contrast Checker.
Check both variants if you're using Tailwind's dark/light mode.
<a
href="#main"
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4
focus:bg-accent-primary focus:px-4 focus:py-2 focus:rounded"
>
Skip to main content
</a>
<main id="main">...</main>
Set up @sentry/nextjs. Upload source maps during the Vercel build. Track releases by commit SHA.
// sentry.server.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.NEXT_PUBLIC_ENV,
release: process.env.NEXT_PUBLIC_COMMIT_SHA,
});
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.05,
replaysOnErrorSampleRate: 1.0,
integrations: [Sentry.replayIntegration()],
});
Vercel Analytics (free tier), Plausible, or PostHog. Prefer GDPR-compliant, cookieless options.
import { Analytics } from '@vercel/analytics/next';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
UptimeRobot (free), Better Stack, or Vercel's built-in monitoring. Slack/email alerts on downtime.
Recommended setup:
Before you push to main:
npm run lint # 0 error
npm run typecheck # 0 error
npm run build # success, no warning
npm run start # smoke test localhost
npx unlighthouse # Lighthouse 90+ across every category
| Category | Target | Ideal |
|---|---|---|
| Performance | 90+ | 95+ |
| Accessibility | 95+ | 100 |
| Best Practices | 95+ | 100 |
| SEO | 95+ | 100 |
Measured on a mid-range mobile device. Almost everyone hits 100 on a laptop — mobile is what's release-critical.
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP | < 2.5s | 2.5-4.0s | > 4.0s |
| CLS | < 0.1 | 0.1-0.25 | > 0.25 |
| INP | < 200ms | 200-500ms | > 500ms |
// app/error.tsx
'use client';
import { useEffect } from 'react';
import * as Sentry from '@sentry/nextjs';
export default function Error({ error, reset }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div>
<h2>Page not found</h2>
<Link href="/">Back to homepage</Link>
</div>
);
}
// app/blog/[slug]/loading.tsx
export default function Loading() {
return <BlogPostSkeleton />;
}
// app/layout.tsx
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link rel="dns-prefetch" href="https://www.googletagmanager.com" />
</head>
{
"framework": "nextjs",
"buildCommand": "npm run build",
"regions": ["fra1"],
"github": {
"silent": false
}
}
fra1 (Frankfurt) region for EU data residency. Multi-region only if you have significant global traffic.
Related articles from us: Headless CMS in Hungary — content management alongside Next.js. Improving webshop conversion rate — e-commerce performance. 7 defenses against LLM hallucinations — if you're building an AI feature into Next.js.
The 25 points aren't exhaustive — a given project may need more. But with this checklist, launch day won't turn into a "we should have done this too" panic.
A pre-launch audit is a 1-2 day process: work through all 25+5 points, measure, and fix the gaps. Don't leave this for the first week of hyper-care — it belongs in the sprint during the final week before launch.
If you're planning a Next.js project for production, let's talk through the deployment strategy — we have a proven approach for every one of the 25 points. On the first release, our partner developer personally walks through the checklist with you, so the handover goes cleanly.
About the author
Corevanix Kft.
Technology partner
Budapest-based technology partner — SAP/ERP integration, web development, AI automation and mobile app development. We work inside the client’s own environment, and the delivered code belongs entirely to the client.

A deep dive into three headless CMS options: Sanity, Strapi and Contentful. Pricing, real-time preview, localisation and five Hungarian-market use cases.

Ten concrete technical optimisations that measurably lift webshop conversion: performance, UX, trust signals, checkout flow and A/B-testable elements.