#Redirects and middleware

1 messages · Page 1 of 1 (latest)

frozen craneBOT
#

Hi @spring canopy. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

The issue you're experiencing—where static assets like CSS are not loading when using middleware to redirect subdomains—is a common pitfall. This happens because your middleware is also intercepting requests for static assets (such as those under /_next/), and redirecting or rewriting them, which leads to 404 errors for those resources.

Solution:
You need to update your middleware to skip processing for static assets and API routes. This way, requests for CSS, JS, images, and other static files are not redirected or rewritten, and will load correctly.

Here's how you can do it in your middleware.ts:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Skip middleware for static files and API routes
  if (
    request.nextUrl.pathname.startsWith('/_next') ||
    request.nextUrl.pathname.startsWith('/api') ||
    request.nextUrl.pathname.startsWith('/images')
  ) {
    return NextResponse.next();
  }

  // Example: Redirect all requests to the admin subdomain to /admin
  const host = request.headers.get('host') || '';
  if (host.startsWith('admin.')) {
    return NextResponse.rewrite(new URL('/admin', request.url));
  }

  return NextResponse.next();
}

This pattern is confirmed as a working solution by users who faced the same issue: by checking the pathname and skipping the middleware for static assets, your CSS and other static files will load as expected, while your subdomain logic will still work for page requests GitHub Issue: middleware rewrite and tailwind.css.

#

Key points:

  • Always skip middleware for paths like /_next, /api, /images, and any other static or public asset routes.
  • This ensures that only actual page requests are handled by your subdomain logic, and static assets are served normally.

Let me know if you need help with your next.config.ts as well!

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane So I don't even need a next.config.ts if i use this middleware?

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

You are correct: if your redirect/rewrite logic is fully handled in middleware.ts, you do not need to define the same logic in next.config.ts. Middleware allows you to programmatically handle redirects, rewrites, and other routing logic before the request is completed, and it runs after any redirects or rewrites defined in next.config.js/ts Middleware documentation.

However, you might still use next.config.ts for other unrelated configuration (e.g., custom webpack config, environment variables, etc.), but for your described use case—redirecting subdomains and skipping static assets—middleware alone is sufficient.

Good to know:

  • If you have a large, static list of redirects or rewrites that never change, you can use next.config.ts for those.
  • If your logic is dynamic or based on request headers, cookies, or other runtime data, middleware is the recommended approach NextResponse.redirect in Middleware.

So, for your scenario, you do not need a next.config.ts for routing if you are handling everything in middleware.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane /** @type {import('next').NextConfig} /
const nextConfig = {
async headers() {
return [
{
source: '/api/:path
',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
],
},
]
},
}

module.exports = nextConfig

enough for the next config or anmything else

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Your next.config.js (or .ts) example sets some important security headers for /api/:path* routes:

/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options',      value: 'DENY'   },
          { key: 'X-XSS-Protection',     value: '1; mode=block' },
        ],
      },
    ]
  },
}

module.exports = nextConfig

However, there are a few things to consider:

  1. Recommended Security Headers:
    According to the official Next.js and Vercel documentation, it is recommended to set the following headers for robust security:

  2. X-XSS-Protection Header:
    The knowledge sources do not mention X-XSS-Protection as a recommended header. Modern browsers have deprecated this header in favor of Content Security Policy (CSP).

  3. Scope:
    Your config only applies these headers to /api/:path*. If you want these headers (or others) to apply to all routes, you should adjust the source pattern accordingly (e.g., /(.*)).

  4. Example of a more complete configuration:

#
const securityHeaders = [
  { key: 'Content-Security-Policy', value: "default-src 'self'" },
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
];

module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

See: Next.js Security Headers Options, Vercel Conformance Security Headers

Summary:
Your config is a good start, but for best practices and to pass security conformance checks, you should add the recommended headers above and consider applying them to all routes, not just /api/:path*. The knowledge sources do not mention X-XSS-Protection as necessary.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
if (
request.nextUrl.pathname.startsWith('/_next') ||
request.nextUrl.pathname.startsWith('/api') ||
request.nextUrl.pathname.startsWith('/images')
) {
return NextResponse.next();
}

const host = request.headers.get('host') || '';
if (host.startsWith('admin.')) {
return NextResponse.rewrite(new URL('/admin', request.url));
}

return NextResponse.next();
}

using this as middleware, when I go to admin.website.com, instead ofshowing me website.com/admin's page it just shows me website.com's page
and to access the admin panel i have to https://admin.website.com/admin/{page}

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

The behavior you're seeing is because NextResponse.rewrite(new URL('/admin', request.url)) rewrites all requests to the admin subdomain to /admin, but it does not preserve the original path. This means that when you visit <https://admin.website.com,> you get website.com/admin, but if you visit <https://admin.website.com/page,> it still rewrites to website.com/admin, not website.com/admin/page.

If you want all routes under the admin subdomain to map to /admin plus the original path, you need to append the original pathname after /admin. For example, <https://admin.website.com/foo> should rewrite to <https://website.com/admin/foo.>

Here's how you can adjust your middleware:

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  if (
    request.nextUrl.pathname.startsWith('/_next') ||
    request.nextUrl.pathname.startsWith('/api') ||
    request.nextUrl.pathname.startsWith('/images')
  ) {
    return NextResponse.next();
  }

  const host = request.headers.get('host') || '';
  if (host.startsWith('admin.')) {
    // Prepend /admin to the original path, unless it's already there
    let newPath = request.nextUrl.pathname;
    if (!newPath.startsWith('/admin')) {
      newPath = '/admin' + newPath;
    }
    // Preserve search params
    if (request.nextUrl.search) {
      newPath += request.nextUrl.search;
    }
    return NextResponse.rewrite(new URL(newPath, request.url));
  }

  return NextResponse.next();
}

This way:

  • <https://admin.website.com> → /admin
  • <https://admin.website.com/foo> → /admin/foo
  • <https://admin.website.com/admin/bar> → /admin/bar (no double /admin/admin)
#

This approach is consistent with how subdomain-based multi-tenant routing is handled in Next.js middleware, as seen in the official multi-tenant example, where the middleware rewrites the path based on the subdomain and preserves the original path and query parameters Next.js Multi-Tenant Example.

If you need more advanced subdomain handling, you can refer to the official multi-tenant starter kit for Next.js, which demonstrates dynamic subdomain routing using middleware Next.js Multi-Tenant Example.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane but i dont want it to add /admin
I just want it to show the /admin page in the subdomain without adding it

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

If you want the admin subdomain (e.g., admin.website.com) to show the /admin page without adding /admin to the URL (so that admin.website.com displays the same content as website.com/admin), you should use a rewrite that maps the root of the subdomain to /admin, but does not change the visible URL.

You can achieve this using a rewrite rule in your next.config.js with the has field to match the host, as described in the official documentation:

module.exports = {
  async rewrites() {
    return [
      {
        source: '/:path*',
        has: [
          {
            type: 'host',
            value: 'admin.website.com',
          },
        ],
        destination: '/admin/:path*',
      },
    ]
  },
}

This configuration will:

  • Rewrite all requests to admin.website.com to /admin/:path* on your main site,
  • But the URL in the browser will remain as admin.website.com.

This approach is recommended for subdomain-to-subpath routing and is documented in the official Vercel and Next.js guides Can I redirect from a subdomain to a subpath?, Header, Cookie, and Query Matching.

You do not need to use middleware for this use case.
Middleware is only necessary if you need dynamic or conditional logic that cannot be handled by static rewrites.


Summary:

  • Use a rewrite in next.config.js with the has field for the host.
  • This will show the /admin page at admin.website.com without changing the URL to /admin.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane this will also rewrite static assets? how do i make them show up

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Great question! According to the official Next.js documentation, when you use rewrites in next.config.js with the has field for subdomain routing, static assets (such as those in /public or /_next/static) are not rewritten by default. This is because of the order in which Next.js processes routes:

  1. headers are checked/applied
  2. redirects are checked/applied
  3. beforeFiles rewrites are checked/applied
  4. Static files from the public directory, _next/static files, and non-dynamic pages are checked/served
  5. afterFiles rewrites are checked/applied
  6. fallback rewrites are checked/applied

This means static assets are served before rewrites are applied, so your CSS, JS, and images will load correctly as long as they exist in the appropriate directories (see official docs, see also).

If you notice static assets are not loading:

  • Double-check that your static files are in the /public directory or are being generated by Next.js in the correct location.
  • Make sure your rewrite rule does not unintentionally match asset paths like /_next/static or /public.

Summary:
You do not need to add special handling for static assets when using rewrites as described—they will be served correctly by default, and your admin subdomain will display the /admin page with all assets loading as expected.

#

If you need to be extra cautious, you can use the beforeFiles phase for your rewrite, which ensures static files are checked first (see docs).

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@spring canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.

spring canopy
#

@frozen crane /** @type {import('next').NextConfig} /
const nextConfig = {
async headers() {
return [
{
source: '/api/:path
',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
],
},
]
},
async redirects() {
return [
{
source: '/admin',
destination: 'https://admin.personaessential.com',
permanent: true, // Or false if it's a temporary redirect
},
{
source: '/admin/:path*',
destination: 'https://admin.personaessential.com/:path*',
permanent: true, // Or false if it's a temporary redirect
},
]
},
async rewrites() {
return [
{
source: '/:path*',
destination: '/:path*', // Default rewrite for personaessential.com
},
{
// This condition ensures this rewrite rule only applies to admin.personaessential.com
has: [
{
type: 'host',
value: 'admin.personaessential.com',
},
],
source: '/:path*',
destination: '/admin/:path*', // Rewrites admin.personaessential.com to /admin
},
]
},
}

module.exports = nextConfig

will this rewrite static abnd api requests?

frozen craneBOT
#

Let me check the knowledge base for you again. Give me one moment! :books:

#

Based on the official Next.js documentation, rewrites in next.config.js are applied after static files and API routes are checked/served. This means that your rewrite rule for admin.personaessential.com:

{
  has: [
    { type: 'host', value: 'admin.personaessential.com' },
  ],
  source: '/:path*',
  destination: '/admin/:path*',
}

will not rewrite requests for static assets or API routes. Static files (such as those in /public or /_next/static) and API routes (/api/:path*) are served before rewrites are applied. This is explicitly stated in the documentation:

static files from the public directory, _next/static files, and non-dynamic pages are checked/served before rewrites are applied
Next.js rewrites order

So, your static assets and API routes will not be affected by this rewrite rule, and they will continue to work as expected.

If you want to be absolutely certain, you can use the beforeFiles phase for your rewrite, which is also applied after headers/redirects but before static files, ensuring the same behavior see docs.

Summary:

  • Your rewrite rule for the admin subdomain will not rewrite static asset or API requests.
  • Static files and API routes are served before rewrites are applied, according to the official Next.js routing order.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: