You’ve just shipped a Next.js 13 site, the pages rank, and then someone on your team asks, “Wait, did we set a CSP? Is that API key in the client bundle?” Security often gets bolted on after launch, and that’s usually when it breaks something search engines care about.
In this post I’ll show you four practical ways to harden a Next.js 13 app (static pages, safer images, a Content Security Policy and protected environment variables) without hurting your SEO.

Pre-render pages with static optimization
Static pages are the easiest security win in Next.js. When a page is pre-rendered to plain HTML at build time, no server-side code runs for it on each request. Less code running per request means fewer places for something to go wrong, and you get a speed boost for free.
Static pages also help SEO, because crawlers get the full content in the HTML. In the Pages Router, you fetch data at build time with getStaticProps. To fetch your data at build time, add this to your page:
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
return { props: { posts } }
}
Next.js runs this function during the build, not in the browser, so the API call and anything it needs stay on the server side. The page ships as ready-made HTML.
For dynamic routes like /blog/[slug], pair it with getStaticPaths so Next.js knows which pages to generate:
export async function getStaticPaths() {
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
return {
paths: posts.map((post) => ({ params: { slug: post.slug } })),
fallback: false,
}
}
With fallback: false, any slug that isn’t in the list returns a 404 instead of hitting your server. Run next build and check the output: static pages are marked as such in the route summary.
Serve images safely with next/image
The next/image component resizes and optimizes images for each device, which speeds up page loads and helps your rankings. The security angle is about what your app agrees to fetch: if anyone can make your image optimizer pull files from any host, that’s a door you didn’t mean to leave open.
To allow remote images only from hosts you trust, add this to next.config.js:
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
},
],
},
}
Any remote image from a host that isn’t on the list gets rejected. Swap images.example.com for your CDN or CMS domain.
Then use the component in your pages. To render an optimized image, write:
import Image from 'next/image'
export default function Hero() {
return (
<Image
src="https://images.example.com/blog/hero.jpg"
alt="Terminal showing a successful next build"
width={1200}
height={630}
/>
)
}
For SEO, I’d suggest three habits: write alt text that describes the image, use filenames that match the content (nextjs-csp-header.png, not IMG_4021.png), and always set width and height so the layout doesn’t jump around while loading.
Add a Content Security Policy to Next.js
A Content Security Policy (CSP) is an HTTP header that tells the browser which sources it may load scripts, styles and other content from. It’s one of your best defenses against Cross-Site Scripting (XSS), where an attacker sneaks their own script into your page.
Next.js lets you send a CSP header for every route. To add one, put this in next.config.js:
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy-Report-Only',
value: "default-src 'self'; img-src 'self' https://images.example.com data:; script-src 'self'",
},
],
},
]
},
}
Notice the Report-Only part. The browser logs violations in the console but doesn’t block anything yet. Once the console stays clean, rename the key to Content-Security-Policy to enforce it.

This is where security and SEO can clash. A strict policy can quietly block your analytics script, a font or a stylesheet, and a broken page doesn’t rank well. Test every directive, check the browser console on your key pages, and add the external domains you actually need before you switch to enforcing mode.
Protect your environment variables
Environment variables are where your secrets live: API keys, database credentials, access tokens. Next.js loads them from a .env.local file at the root of your project, and you can read them at build time or at runtime.
A typical .env.local looks like this:
DATABASE_URL=postgres://user:password@localhost:5432/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
The prefix matters. Variables without a prefix stay on the server. Anything starting with NEXT_PUBLIC_ is baked into the JavaScript sent to the browser, so anyone can read it. Never put a secret behind that prefix.

Make sure the file never lands in Git. To add it to your ignore list, run:
echo ".env*.local" >> .gitignore
Then run git status and confirm .env.local no longer shows up as a new file.
Beyond that, don’t hardcode secrets in your code, ever. For production, I’d suggest a proper secrets management system so keys stay out of your repo and your build logs. A leaked key that gets your site defaced or flagged as unsafe will hurt your search traffic far more than any missing meta tag.
Host your Next.js app on Ubuntu
Once your app is locked down, you need somewhere to run it. If you want to self-host, here’s my guide to deploying Next.js 13 with Nginx on Ubuntu, with HTTP/2, gzip and static caching. If your app talks to a database, my post on security best practices with tRPC and Prisma covers the API side, and you can keep an eye on everything with Grafana and Docker Compose for Next.js apps.
Hope this saves you a late-night security scare. 🔒
Leave a Reply