Nexjs14 App Router : Tailwind styles not applied in Multi-tenant subdomains
Unanswered
Weevil parasitoid posted this in #help-forum

Weevil parasitoidOP
theres an issue i cant seem to figure out in fixing it .. this issue is happening production(vercel) ... the project im working on is a a multi tenant page builder project , where users can build their own website using the platform i built using nextjs 14 App router with Grapesjs.. the main issue i have right now ....is when i access the created website through the platform.. eg: heavy.web.page ... everything shows but it looks not styled everything is out of place ... when i inspect the requests i get 404 errors .. here is the code that im working with below:

6 Replies

Weevil parasitoidOP
// app/[domain]/page.tsx
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { prisma } from "@/lib/prisma";
import { getProjectMetadata } from '@/components/domain/metadata';
import { Circle } from 'lucide-react';
import { sanitizeCSS, sanitizeHTML } from '@/lib/serverDOMPurify';
//import parse from 'html-react-parser';
export const dynamic = 'force-dynamic'
//export const revalidate = 60; // Revalidate every minute
interface PageProps {
params: {
domain: string;
};
}
// Define a type for the project data
interface ProjectData {
pagesData?: Array<{
html: string;
css: string;
name?: string;
metadata?: Record<string, string>;
}>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
return getProjectMetadata(params.domain);
}
export default async function DomainPage({ params }: PageProps) {
const { domain } = params;
// Use Prisma's JSON type and specify the expected structure
const project = await prisma.project.findFirst({
where: {
OR: [
{ subdomain: domain },
{ customDomain: domain }
]
},
select: {
data: true,
isPublished: true
}
});
// Safely parse the project data
let projectData: ProjectData | null = null;
try {
projectData = project.data as ProjectData;
} catch (error) {
return notFound();
}
// Get the first page (home page)
const mainPage = projectData?.pagesData?.[0];
if (!mainPage) {
return notFound();
}
// Sanitize HTML and CSS
const sanitizedHTML = sanitizeHTML(mainPage.html || '');
const sanitizedCSS = sanitizeCSS(mainPage.css || '');
// Server-side rendering of HTML and CSS
return (
<>
<style dangerouslySetInnerHTML={{ __html: sanitizedCSS }} />
<div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />
</>
);
}
// app/[domain]/layout.tsx
import React from "react";
import { Metadata } from 'next';
import { getProjectMetadata } from '@/components/domain/metadata';
export const dynamic = 'force-dynamic';
interface LayoutProps {
children: React.ReactNode
params: { domain: string }
}
export async function generateMetadata({
params
}: {
params: { domain: string }
}): Promise<Metadata> {
return getProjectMetadata(params.domain)
}
export default async function DomainLayout({
children,
params
}: LayoutProps) {
return (
<div>
{children}
</div>
)
}
/** @type {import('next').NextConfig} */
const nextConfig = {
async rewrites() {
return {
beforeFiles: [
{
source: '/:path*',
has: [
{
type: 'host',
value: '(?<domain>.*)\\.web\\.page'
}
],
destination: '/:domain/:path*'
}
]
};
},
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
},
{
protocol: 'https',
hostname: 'dummyimage.com',
},
],
},
};
export default nextConfig;
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
export const auth = NextAuth(authConfig).auth;
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)",
],
};
export default async function middleware(req: NextRequest) {
try {
const hostname = req.headers.get("host") || 'NO_HOST_FOUND';
const path = req.nextUrl.pathname;
const searchParams = req.nextUrl.searchParams.toString();
// Normalize hostname
const normalizedHostname = hostname.replace(/^www\./, '');
// Check if it's a subdomain
const hostParts = normalizedHostname.split('.');
const isSubdomain =
hostParts.length > 2 &&
hostParts[0] !== 'www' &&
normalizedHostname !== process.env.NEXT_PUBLIC_ROOT_DOMAIN;
// Extract subdomain
const subdomain = isSubdomain ? hostParts[0] : null;
// Construct full path with search params
const fullPath = `${path}${searchParams.length > 0 ? `?${searchParams}` : ""}`;
// Subdomain routing
if (subdomain) {
console.log('Subdomain detected:', subdomain);
// Rewrite to subdomain-specific route
const rewriteUrl = new URL(`/${subdomain}${fullPath}`, req.url);
console.log('Rewriting to:', rewriteUrl.toString());
return NextResponse.rewrite(rewriteUrl);
}
// Default routing
return NextResponse.rewrite(new URL(fullPath, req.url));
} catch (error) {
console.error('Middleware Error:', error);
return NextResponse.redirect(new URL('/', req.url));
}
}

@joulev Check if the incoming pathname starts with _next
If yes, simply continue (NextResponse.next)
If no, rewrite as usual

Weevil parasitoidOP
did that just now , the issue is persisting