fix production prerender without auth config
CI / Verify (push) Successful in 55s
CI / Build immutable images and deploy (push) Successful in 1m42s

This commit is contained in:
2026-08-31 00:59:20 +07:00 Unverified
parent e3692fe75b
commit 4bf9d9098f
3 changed files with 43 additions and 10 deletions
+5
View File
@@ -0,0 +1,5 @@
import { Suspense } from "react";
export default function AdminLayout({ children }: LayoutProps<"/admin">) {
return <Suspense fallback={null}>{children}</Suspense>;
}
+27 -10
View File
@@ -1,15 +1,14 @@
"use client"
import { Suspense, useEffect, useRef, useState } from "react"
import { usePathname, useSearchParams } from "next/navigation"
import { useEffect, useRef, useState } from "react"
import { Progress } from "@/components/ui/progress"
function PageProgressContent() {
const pathname = usePathname()
const searchParams = useSearchParams()
const [progress, setProgress] = useState(0)
const [isVisible, setIsVisible] = useState(false)
const [navigationKey, setNavigationKey] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const navigationUrlRef = useRef<string | null>(null)
// Start progress on click
useEffect(() => {
@@ -26,6 +25,7 @@ function PageProgressContent() {
// Only show progress for actual page transitions
if (isInternal && !isSamePage && target.getAttribute("target") !== "_blank") {
navigationUrlRef.current = window.location.href
setIsVisible(true)
setProgress(15)
@@ -48,6 +48,26 @@ function PageProgressContent() {
return () => document.removeEventListener("click", handleClick)
}, [])
// Detect completion without reading Next.js URL hooks during prerendering.
useEffect(() => {
if (!isVisible || !navigationUrlRef.current) return
const initialUrl = navigationUrlRef.current
const checkInterval = window.setInterval(() => {
if (window.location.href !== initialUrl) {
window.clearInterval(checkInterval)
setNavigationKey((key) => key + 1)
}
}, 50)
const fallback = window.setTimeout(() => {
window.clearInterval(checkInterval)
setNavigationKey((key) => key + 1)
}, 10_000)
return () => {
window.clearInterval(checkInterval)
window.clearTimeout(fallback)
}
}, [isVisible])
// Finish progress on route change
useEffect(() => {
if (intervalRef.current) {
@@ -60,6 +80,7 @@ function PageProgressContent() {
const timeout = setTimeout(() => {
setIsVisible(false)
setTimeout(() => setProgress(0), 300) // Reset after hiding
navigationUrlRef.current = null
}, 400)
return () => {
clearTimeout(finishTimeout)
@@ -67,7 +88,7 @@ function PageProgressContent() {
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname, searchParams])
}, [navigationKey])
if (!isVisible && progress === 0) return null
@@ -82,9 +103,5 @@ function PageProgressContent() {
}
export function PageProgress() {
return (
<Suspense fallback={null}>
<PageProgressContent />
</Suspense>
)
return <PageProgressContent />
}
+11
View File
@@ -26,6 +26,12 @@ function required(name: string): string {
return value;
}
function hasAuthConfiguration(): boolean {
return ["DATABASE_URL", "BETTER_AUTH_URL", "BETTER_AUTH_SECRET", "ADMIN_EMAIL"].every(
(name) => Boolean(process.env[name]),
);
}
function createAuth() {
const adminEmail = required("ADMIN_EMAIL");
@@ -109,6 +115,11 @@ export async function getAdminSession(): Promise<AdminSession | null> {
};
}
// Public pages can be prerendered without the runtime auth secret. In that
// case the header simply omits the admin link; auth routes still fail loudly
// through getAuth() when authentication is actually used.
if (!hasAuthConfiguration()) return null;
const session = await getAuth().api.getSession({ headers: await headers() });
if (
!session?.session ||