vercel / platforms

A full-stack Next.js app with multi-tenancy and custom domain support. Built with Next.js App Router and the Vercel Domains API.
https://app.vercel.pub
5.73k stars 767 forks source link

How to load different UI configurations based on the tenant? #378

Open rpfelgueiras opened 11 months ago

rpfelgueiras commented 11 months ago

I see that Hashnode is loading a different theme (or UI settings) based on the tenant. How do I do the same?

chrishoermann commented 10 months ago

I currently have the same requirement and had no luck with radix-theme yet - see #379

rpfelgueiras commented 10 months ago

@chrishoermann I was searching for a solution and I will probably go in this direction https://github.com/tailwindlabs/tailwindcss/discussions/11964

In build time generate the right theme configurations per tenant and place the output in the right server/folder.

alexanderbnelson commented 10 months ago

I have theming implemented in my app. It's a multi-faceted approach that involves a combination of dynamically importing css and objects with tailwind classes.

Essentially all you need to do is get your subdomain or domain from next/headers at the root layout. Then use that to dynamically load your required theming info.

export default async function RootLayout(props: LayoutProps) {

  const headersList = headers();
  const host = headersList.get('host');
  const site = await getSiteData(host);

  ...

In my case getSiteData() looks like this - I have my hostname parsing function here

// Extract subdomain helper
function extractSubdomain(domain: string) {

  if (domain.endsWith(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`)) {
    return domain.replace(`.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`, '');
  } else if (domain.endsWith('.localhost:3000')) {
    return domain.replace('.localhost:3000', '');
  }

  return null;
}

export async function getSiteData(domain: string | null) {
  if (!domain) {
    return console.log("No domain provided");;
  }

  const subdomain = extractSubdomain(domain);

  return await unstable_cache(
    async () => {
      return prisma.site.findUnique({
        where: subdomain ? { subdomain: subdomain } : { customDomain: domain },
        include: {
          user: true,
          activeTheme: true,
        },
      });
    },
    [`${domain}-metadata`],
    {
      revalidate: 900,
      tags: [`${domain}-metadata`],
    },
  )();
}

Colour and font tokens are set by CSS variables and called for at the root with a data attribute. Those CSS variables are also set into my tailwind.config, just like @rpfelgueiras has shown in the link he referenced.

Set your data attributes on your tag. This is where you want font definitions especially

<html
      { "data-color-theme": `theme-${themeName}-color` }
      { "data-font-theme": `theme-${themeName}-font` }
    >

Define CSS variables

@layer base {

  [data-color-theme='theme-themeName-color'] { 
    --background: 0 0% 99%;
    --foreground: 45 25% 5%;

    --primary: 45 25% 5%;
    --primary-foreground: 45 25% 98%;
    ...

Then, tailwind classes are put into a theme-{themeName}.tsx file and applied in the site template code after dynamically importing the tailwind classes

Theme file

const themeName: Theme = {
    name: "The Theme Name",
    global: {
      text: "text-primary font-light",
      buttonRadius: "rounded-full"
    },
    ...

Dynamic import. This depends on naming your files with the same values you're using in your database to identify a site's theme.

const themeData = await import(`@/styles/themes/theme-${themeName}/theme-${themeName}`);`

Applied in components

<div className={cn(theme.profile.container)}>
        <p className={cn(theme.profile.name)}>{data?.displayName}</p>
        <div className={cn(theme.profile.infoContainer)}>
          ...

In my case, I'm not swapping out large chunks of JSX (yet). I'm just changing style. But there's no stopping you from having entire components conditionally render this way. It's a bit tedious to set up initially. But once the architecture is done, creating new themes is a bit less work.

tomgreeEn commented 10 months ago

I have a ServerSideThemeWrapper wrapping everything in the [domain] route, which has access to the relevant Site, from which I pass the theme props.

Using shadcn/ui:


export function ServersideThemeWrapper({
  children,
  className,
  color,
  radius,
}: ServersideThemeWrapperProps) {
  return (
    <div
      className={cn(`theme-${color}`, "w-full", className)}
      style={
        {
          "--radius": `${radius ? radius : 0.5}rem`,
        } as React.CSSProperties
      }
    >
      {children}
    </div>
  );
}
mworks-proj commented 6 months ago

I have a ServerSideThemeWrapper wrapping everything in the [domain] route, which has access to the relevant Site, from which I pass the theme props.

Using shadcn/ui:


export function ServersideThemeWrapper({

  children,

  className,

  color,

  radius,

}: ServersideThemeWrapperProps) {

  return (

    <div

      className={cn(`theme-${color}`, "w-full", className)}

      style={

        {

          "--radius": `${radius ? radius : 0.5}rem`,

        } as React.CSSProperties

      }

    >

      {children}

    </div>

  );

}

Mind sharing repo projects so far?