Search code examples
javascriptauthenticationnext.jsclerkstatic-site-generation

NextJS14 Static Site Generation (SSG) and Authentication using Clerk


I'm facing a classic dilemma with Next.js Static Site Generation (SSG) and protected routes. I understand that SSG requires data to be available at build time, while protected routes depend on user authentication, which isn't available during this phase. The pages I'm trying to load require some heavy call to a database. Once these call are made, the information will most likely remains static as changes occur once a day, at night, where a revalidation process is happening.

Is there a way to bypass at building time the Authentication phase so that the "npm run build" process still generate correctly a pre rendering of these pages, while keeping them protected at production time ?

the classic implementation I understand for Clerk is to adress first the Layout layer

import { ClerkProvider, SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
import './globals.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <header>
            <SignedOut>
              <SignInButton />
            </SignedOut>
            <SignedIn>
              <UserButton />
            </SignedIn>
          </header>
          <main>{children}</main>
        </body>
      </html>
    </ClerkProvider>
  )
}

but them let's say one of my pages is like this:

import {getAllEmployeesFromDatabase} from "@/lib/serverEmployeeLib";
import{ EmployeeDataTable} from "@/components/EmployeeDataTable"
import React from "react";

async function EmployeesPage() {
  const employeeData = await getAllEmployeesFromDatabase(); //server action

  return (
    <div>
      <EmployeeDataTable
        data={employeeData}
      />
    </div>
  );
}

export default EmployeesPage;

Then the getAllEmployeesFromDatabase function won't be called at build time if the "EmployeesPage" is part of the protected route.

I'm tempted to implement the following, but that looks ugly and wrong as it defies the purpose of having a simple and centralize protected layout.... It would force me to add this to every single pages

import { getAllEmployeesFromDatabase } from "@/lib/serverEmployeeLib";
import { EmployeeDataTable } from "@/components/EmployeeDataTable";
import React from "react";

async function EmployeesPage() {
  const employeeData = await getAllEmployeesFromDatabase();

  return (
    <ClerkProvider>
      <SignedOut>
        <SignInButton />
      </SignedOut>
      <SignedIn>
        <UserButton />
      </SignedIn>
      <div>
        <EmployeeDataTable data={employeeData} />
      </div>
    </ClerkProvider>
  );
}

export default EmployeesPage;

Any Idea on how to do that properly ? Thanks

*Edit *

just keeping track of things I've tried... I've been trying to bypass the build process with an env variable and the cross-env npm package.

in the scripts part of my package.json file, I added the following:

"scripts": {
    "dev": "next dev",
    "build": "next build",
    "build_auth": "cross-env NEXT_PUBLIC_BYPASS_AUTH=true next build",
    "start": "next start",
    "lint": "next lint",
    "email": "email dev --dir src/emails"
  },

then, I transformed the layout as follow:

import {
  ClerkProvider,
  SignInButton,
  SignedIn,
  SignedOut,
  UserButton,
} from "@clerk/nextjs";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const is_building = (process.env.NEXT_PUBLIC_BYPASS_AUTH ?? false) === "true";
  return (
    <>
      {!is_building && (
        <ClerkProvider>
          <html lang="en">
            <body>
              <header>
                <SignedOut>
                  <SignInButton />
                </SignedOut>
                <SignedIn>
                  <UserButton />
                </SignedIn>
              </header>
              <main>{children}</main>
            </body>
          </html>
        </ClerkProvider>
      )}
      {is_building && (
        <html lang="en">
          <body>
            <header></header>
            <main>{children}</main>
          </body>
        </html>
      )}
    </>
  );
}

and then ran "npm run build_auth".

So that worked well. It definitely generated a static rendering for all the children. But also the layout ! Which means at production time, npm run start, all the authentication process is lost... the !is_building part is never evaluated as it has pre-rendered the is_buiding part. So the ClerkProvider context just does not exist. So how can I force the pure layout part to be dynamic while {children} part be static ?

** Edit 2 **

Looks like a limitation from Cleck... This post explains the problem

The latest information suggests that,

as of @clerk/[email protected], rendering ClerkProvider does not automatically force a page to be client-side rendered. However, due to the dynamic nature of authentication data, placing it at the top level does opt the page into dynamic rendering

This is confusing...


Solution

  • Honestly, it's a bit of a mystery, but it worked for me and got the job done.

    ClerkProvider can be rendered as a client component, and when wrapped in a suspense boundary next is able to continue static generation of the rest of the page.

    Taken from the same github issue question.

    So the solution might be into the new PPR feature of Next.js

    "use client";
    
    import { Suspense } from "react";
    import { ClerkProvider, SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
    import './globals.css'
    
    export const experimental_ppr = true;
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
       <Suspense>
        <ClerkProvider>
          <html lang="en">
            <body>
              <header>
                <SignedOut>
                  <SignInButton />
                </SignedOut>
                <SignedIn>
                  <UserButton />
                </SignedIn>
              </header>
              <main>{children}</main>
            </body>
          </html>
        </ClerkProvider>
      </Suspense>
      )
    }