Skip to Content
Web DevlopmentNext.js Middleware Subdomain Config

NextJS Middleware Subdomain Configuration

Next.js middleware allows you to run code before a request is completed, enabling you to modify the response or redirect users based on certain conditions.

Common use case

Suppose you have a portfolio page. By default, it is accessible at http://localhost:3000/portfolio . Later, you want it available under a subdomain, e.g., portfolio.your-domain.com.

A naive approach would be to create a separate Next.js project for the portfolio, but that wastes resources.

Next.js middleware  provides a cleaner solution. You can explicitly redirect or block requests. For example, requests to your-domain.com/portfolio can be redirected to your main page (your-domain.com), effectively blocking direct access via the path.

At the same time, the middleware can allow requests from portfolio.your-domain.com to reach the portfolio resource(your-domain.com/portfolio). This ensures the portfolio page is only accessible through the designated subdomain.

Create the middleware

I’m using TypeScript in my project but the code is nearly the same for JavaScript. If you still have problems read the docs for Next.js middleware .

Create the middleware file

Create a file named middleware.ts in your /app directory. If your using a /src directory, place it there, it needs to be on the same level as your /app directory. This config is also working for local development, f.e. http://portfolio.localhost:3000 will work too.

Add the code to the file

This is an example configuration for the portfolio. subdomain.
If you are using a different subdomain name, change it in the highlighted lines.

middleware.ts
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const ASSET_EXTENSIONS = [ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', ]; export function middleware(request: NextRequest) { const url = request.nextUrl; // Get hostname (e.g., 'portfolio.example.com' or 'portfolio.localhost:3000') const hostname = request.headers.get('host') || ''; const pathname = url.pathname; // Skip middleware for static assets under 'public' by extension or known paths const isStaticAsset = pathname.startsWith('/_next/static') || pathname.startsWith('/_next/image') || pathname === '/favicon.ico' || pathname === '/sitemap.xml' || pathname === '/robots.txt' || ASSET_EXTENSIONS.some(ext => pathname.toLowerCase().endsWith(ext)); if (isStaticAsset) { return NextResponse.next(); } // Clean the hostname: remove port numbers (crucial for local dev) // and standard 'www' prefix if you want to normalize it. const currentHost = hostname.split(':')[0].replace('www.', ''); // ----------------------------------------------------------- // SECURITY CHECK (HOST HEADER VALIDATION) // ----------------------------------------------------------- const allowedDomains = [ '<your-domain>', 'localhost', ]; const isAllowedHost = allowedDomains.includes(currentHost) || currentHost.endsWith('.<your-domain>'); if (!isAllowedHost) { return new NextResponse('Bad Host Header', { status: 400 }); } // Define your subdomain logic const isPortfolioSubdomain = currentHost.startsWith('portfolio.'); // --- DEBUGGING (Remove in production if logs get too noisy) --- // console.log(`[MW] Host: ${currentHost} | Path: ${url.pathname} | IsPortfolio: ${isPortfolioSubdomain}`); // 1. SUBDOMAIN ROUTING // If the user is on 'portfolio.domain.com', rewrite the content to display '/portfolio' if (isPortfolioSubdomain) { // We rewrite the path to the internal /portfolio directory // Example: portfolio.com/about -> rewritten to /portfolio/about const newUrl = new URL(`/portfolio${url.pathname}`, request.url); // Preserve query parameters newUrl.search = url.search; return NextResponse.rewrite(newUrl); } // 2. PROTECT INTERNAL ROUTES // Block direct access to example.com/portfolio. // Force users to use the subdomain or redirect them to the home page. if (url.pathname.startsWith('/portfolio')) { // Redirect to the root of the main domain return NextResponse.redirect(new URL('/', request.url)); } // 3. DEFAULT return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico, sitemap.xml, robots.txt (metadata files) */ '/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', ], };

You can now access your page only via your configured subdomain, access via /portfolio will be blocked / redirected to the root of your domain.

Created: 20.10.2025

Last Updated: 10.12.2025