Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/app/layout.tsx
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { cookies } from "next/headers";
import { Inter, La_Belle_Aurore } from "next/font/google";
import { RootShell } from "@/components/layout/root-shell";
import { FeaturedPostProvider } from "@/components/layout/featured-post";
Expand All@@ -9,7 +10,7 @@ import { PageTracker } from "@/components/analytics/page-tracker";
// Imported from the plain module, never from the "use client" provider — a
// server importer of a client export gets a throwing proxy, not the string.
import { THEME_INIT_SCRIPT } from "@/components/theme/theme-script";
import { AUTH_INIT_SCRIPT } from "@/lib/auth-script";
import { AUTH_INIT_SCRIPT, SESSION_COOKIE, AUTH_ATTRIBUTE } from "@/lib/auth-script";
import { SITE_NAME, SITE_URL } from "@/lib/constants";
import { getFeaturedPost } from "@/lib/ghost";
import { PUBLISHER } from "@/lib/blog-schema";
Expand DownExpand Up@@ -127,6 +128,9 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieStore = await cookies();
const isAuthed = !!cookieStore.get(SESSION_COOKIE)?.value;
Comment on lines +131 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2Root layout forces dynamic rendering

Calling the request-bound cookies() API in the shared root layout prevents routes beneath it from retaining their intended static or ISR rendering behavior, adding request-time work and conflicting with pages explicitly pinned to build-time generation.

Knowledge Base Used:Site shell and shared UI

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


// Read here rather than in the nav so the strip is server-rendered with the
// page instead of popping in. Ghost's fetches are revalidate-cached and deduped
// per request, so this costs the layout nothing after the first hit.
Expand All@@ -136,10 +140,12 @@ export default async function RootLayout({
<html
lang="en"
className={`h-full antialiased ${inter.variable} ${laBelleAurore.variable}`}
// The pre-paint scripts set data-theme and data-authed on <html> before
// hydration, so the server markup (no attributes) and client differ by
// design.
// data-theme is set by the pre-paint script before hydration; data-authed
// is set here server-side (the session cookie is HttpOnly). The demo
// override script may change data-authed client-side, so both attributes
// can differ from the server markup by design.
suppressHydrationWarning
{...{ [`data-${AUTH_ATTRIBUTE}`]: isAuthed ? "1" : "0" }}
>
<head>
<meta name="facebook-domain-verification" content="73ez73pudx2usus1si5il3vmxyvh5p" />
Expand Down
27 changes: 9 additions & 18 deletions src/lib/auth-script.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,12 +4,9 @@
// error rather than the script.

/**
* Whether the visitor is signed in to Assembly. The dashboard sets a cookie on
* the shared `.assembly.com` parent domain, which any page under that domain
* can read client-side.
*
* A Vercel preview URL is not under `.assembly.com`, so the cookie is never
* visible there — use the demo override below to see both states.
* The dashboard's session cookie. HttpOnly, so it can only be read server-side
* — the root layout reads it with `cookies()` and stamps `data-authed` on
* `<html>` at render time.
*/
export const SESSION_COOKIE = "current-portal-session";

Expand All@@ -23,16 +20,10 @@ export const DEMO_KEY = "studio:demo-authed";
export const AUTH_ATTRIBUTE = "authed";

/**
* Runs before paint so the right variant is on <html> before React hydrates.
*
* This is what makes the auth-aware UI flash-free. The markup is prerendered
* without knowing who is visiting, so every auth-dependent surface ships BOTH
* variants and the stylesheet picks one off this attribute (see `.auth-only` /
* `.unauth-only` in globals.css). Deciding in an effect instead meant a
* signed-in visitor watched the nav, the pricing grid and the comparison table
* render signed-out and then rearrange.
*
* Failing closed to "0" matters: signed-out is the state the markup is written
* for, so a thrown script leaves a correct page rather than a blank one.
* Client-side script that handles only the demo override. The real auth state
* is set server-side on `<html data-authed>` by the root layout (which can
* read the HttpOnly session cookie). This script checks for `?authed=1|0` in
* the URL or a persisted override in localStorage and, if found, overwrites
* the server-set value. If no override is active it leaves the attribute alone.
*/
export const AUTH_INIT_SCRIPT = `try{var a=null;var q=new URLSearchParams(window.location.search).get('authed');if(q==='clear'){localStorage.removeItem('${DEMO_KEY}');}else if(q==='1'||q==='0'){localStorage.setItem('${DEMO_KEY}',q);a=q==='1';}if(a===null){var s=localStorage.getItem('${DEMO_KEY}');if(s==='1'||s==='0')a=s==='1';}if(a===null)a=document.cookie.split('; ').some(function(c){return c.indexOf('${SESSION_COOKIE}=')===0&&c.length>${SESSION_COOKIE.length + 1};});document.documentElement.dataset.${AUTH_ATTRIBUTE}=a?'1':'0';}catch(e){document.documentElement.dataset.${AUTH_ATTRIBUTE}='0';}`;
export const AUTH_INIT_SCRIPT = `try{var a=null;var q=new URLSearchParams(window.location.search).get('authed');if(q==='clear'){localStorage.removeItem('${DEMO_KEY}');}else if(q==='1'||q==='0'){localStorage.setItem('${DEMO_KEY}',q);a=q==='1';}if(a===null){var s=localStorage.getItem('${DEMO_KEY}');if(s==='1'||s==='0')a=s==='1';}if(a!==null)document.documentElement.dataset.${AUTH_ATTRIBUTE}=a?'1':'0';}catch(e){}`;