perf : improve via lighthouse
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { publicSections, StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getCachedPublicGuide } from "@/lib/cache/public";
|
||||
@@ -10,14 +9,12 @@ type Params = Promise<{ character: string; page: string }>;
|
||||
export const instant = false;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {
|
||||
await connection();
|
||||
const { character } = await params;
|
||||
const guide = await getCachedPublicGuide(character);
|
||||
return guide ? { title: `${guide.name}`, description: guide.overview } : { title: "ไม่พบไกด์" };
|
||||
}
|
||||
|
||||
export default async function GuideSectionPage({ params }: { params: Params }) {
|
||||
await connection();
|
||||
const { character, page } = await params;
|
||||
const guide = await getCachedPublicGuide(character);
|
||||
if (!guide || !publicSections(guide).some((section) => section.slug === page)) notFound();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { StructuredGuideShell } from "@/components/public/structured-guide";
|
||||
import { getCachedPublicGuide } from "@/lib/cache/public";
|
||||
@@ -14,7 +13,6 @@ export async function generateMetadata({
|
||||
}: {
|
||||
params: Params;
|
||||
}): Promise<Metadata> {
|
||||
await connection();
|
||||
const guide = await getCachedPublicGuide((await params).character);
|
||||
return guide
|
||||
? { title: `${guide.name}`, description: guide.overview }
|
||||
@@ -26,7 +24,6 @@ export default async function GuideOverviewPage({
|
||||
}: {
|
||||
params: Params;
|
||||
}) {
|
||||
await connection();
|
||||
const guide = await getCachedPublicGuide((await params).character);
|
||||
if (!guide) notFound();
|
||||
return <StructuredGuideShell guide={guide} />;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
export default function AdminLayout({ children }: LayoutProps<"/admin">) {
|
||||
return <Suspense fallback={null}>{children}</Suspense>;
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Suspense fallback={null}>{children}</Suspense>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+9
-13
@@ -4,7 +4,6 @@ import { Anuphan, Geist_Mono } from "next/font/google";
|
||||
import { DeploymentUpdateNotifier } from "@/components/deployment-update-notifier";
|
||||
import { PageProgress } from "@/components/page-progress";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
@@ -28,8 +27,6 @@ export const metadata: Metadata = {
|
||||
description: "ไกด์ตัวละครจากช่องเกนชินไม่ใช่เกมมือถือ",
|
||||
};
|
||||
|
||||
export const instant = false;
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const deploymentId = process.env.NEXT_DEPLOYMENT_ID?.trim() || "development";
|
||||
|
||||
@@ -39,16 +36,15 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
className={`dark ${anuphan.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="flex min-h-full flex-col">
|
||||
<TooltipProvider>
|
||||
<PageProgress />
|
||||
{children}
|
||||
<DeploymentUpdateNotifier deploymentId={deploymentId} />
|
||||
<Toaster
|
||||
richColors
|
||||
closeButton
|
||||
position="bottom-right"
|
||||
toastOptions={{ className: anuphan.className }}/>
|
||||
</TooltipProvider>
|
||||
<PageProgress />
|
||||
{children}
|
||||
<DeploymentUpdateNotifier deploymentId={deploymentId} />
|
||||
<Toaster
|
||||
richColors
|
||||
closeButton
|
||||
position="bottom-right"
|
||||
toastOptions={{ className: anuphan.className }}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { LiveRefresh } from "@/components/live-refresh";
|
||||
import { SiteFooter } from "@/components/public/site-footer";
|
||||
import { StructuredDirectory } from "@/components/public/structured-directory";
|
||||
import { SiteHeader } from "@/components/public/site-header";
|
||||
import { getCachedPublicGuideCards } from "@/lib/cache/public";
|
||||
|
||||
export const instant = false;
|
||||
|
||||
export default async function HomePage() {
|
||||
await connection();
|
||||
const guides = await getCachedPublicGuideCards();
|
||||
return (
|
||||
<div className="flex min-h-svh flex-col bg-background">
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { VisitorPresence } from "@/components/public/visitor-presence";
|
||||
import { getAdminSession } from "@/lib/auth/server";
|
||||
|
||||
export async function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string; guideId?: string } = {}) {
|
||||
async function AdminEditLink({ editHref }: { editHref: string }) {
|
||||
const isAdmin = Boolean(await getAdminSession());
|
||||
|
||||
return isAdmin ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href={editHref} />}
|
||||
>
|
||||
ไปหน้าแก้ไข
|
||||
</Button>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export function SiteHeader({ editHref = "/admin", guideId }: { editHref?: string; guideId?: string } = {}) {
|
||||
return (
|
||||
<header className="border-b bg-background/95 supports-backdrop-filter:backdrop-blur">
|
||||
<div className="mx-auto flex min-h-16 w-full max-w-7xl flex-wrap items-center gap-2 px-4 py-2 sm:px-6">
|
||||
@@ -20,7 +34,9 @@ export async function SiteHeader({ editHref = "/admin", guideId }: { editHref?:
|
||||
</Link>
|
||||
<nav aria-label="Public navigation" className="flex items-center gap-1">
|
||||
<VisitorPresence guideId={guideId} />
|
||||
{isAdmin ? <Button variant="ghost" size="sm" nativeButton={false} render={<Link href={editHref} />}>ไปหน้าแก้ไข</Button> : null}
|
||||
<Suspense fallback={null}>
|
||||
<AdminEditLink editHref={editHref} />
|
||||
</Suspense>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -58,7 +58,7 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
</InputGroup>
|
||||
{filtered.length ? (
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{filtered.map((guide) => (
|
||||
{filtered.map((guide, index) => (
|
||||
<Link
|
||||
key={guide.id}
|
||||
href={`/${guide.slug}`}
|
||||
@@ -70,20 +70,21 @@ export function StructuredDirectory({ guides }: { guides: GuideCard[] }) {
|
||||
src={guide.coverUrl}
|
||||
alt={`ไกด์ ${guide.name}`}
|
||||
fill
|
||||
preload={index === 0}
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<CardHeader className="pt-5">
|
||||
<CardTitle>{guide.name}</CardTitle>
|
||||
{(presence?.byGuide[guide.id] ?? 0) > 0 ? (
|
||||
<CardAction>
|
||||
<Badge variant="secondary">
|
||||
<UsersIcon data-icon="inline-start" />
|
||||
{presence?.byGuide[guide.id]} คน
|
||||
</Badge>
|
||||
</CardAction>
|
||||
) : null}
|
||||
<CardAction
|
||||
className={(presence?.byGuide[guide.id] ?? 0) > 0 ? undefined : "invisible"}
|
||||
>
|
||||
<Badge variant="secondary" className="min-w-14 tabular-nums">
|
||||
<UsersIcon data-icon="inline-start" />
|
||||
{presence?.byGuide[guide.id] ?? 0} คน
|
||||
</Badge>
|
||||
</CardAction>
|
||||
<CardDescription className="line-clamp-3">
|
||||
{guide.overview}
|
||||
</CardDescription>
|
||||
|
||||
@@ -9,13 +9,21 @@ export function VisitorPresence({ guideId }: { guideId?: string }) {
|
||||
const counts = useVisitorPresence(guideId ?? null);
|
||||
return (
|
||||
<div className="flex items-center gap-1" aria-live="polite">
|
||||
<Badge variant="secondary" title="ผู้เข้าชมทั้งหมดที่กำลังออนไลน์">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="min-w-24 tabular-nums"
|
||||
title="ผู้เข้าชมทั้งหมดที่กำลังออนไลน์"
|
||||
>
|
||||
<UsersIcon data-icon="inline-start" />
|
||||
ทั้งหมด {counts?.global ?? ""}
|
||||
ทั้งหมด {counts?.global ?? "–"}
|
||||
</Badge>
|
||||
{guideId ? (
|
||||
<Badge variant="outline" title="ผู้เข้าชม Guide นี้ที่กำลังออนไลน์">
|
||||
Guide นี้ {counts?.guide ?? ""}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="min-w-24 tabular-nums"
|
||||
title="ผู้เข้าชม Guide นี้ที่กำลังออนไลน์"
|
||||
>
|
||||
Guide นี้ {counts?.guide ?? "–"}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
async function repositoryFile(path: string): Promise<string> {
|
||||
return readFile(new URL(`../${path}`, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
describe("public rendering performance", () => {
|
||||
it("keeps the directory eligible for a populated static shell", async () => {
|
||||
const [layout, page] = await Promise.all([
|
||||
repositoryFile("app/layout.tsx"),
|
||||
repositoryFile("app/page.tsx"),
|
||||
]);
|
||||
|
||||
expect(layout).not.toContain("export const instant = false");
|
||||
expect(page).not.toContain("connection()");
|
||||
expect(page).not.toContain("export const instant = false");
|
||||
});
|
||||
|
||||
it("isolates request-time header authentication", async () => {
|
||||
const header = await repositoryFile("components/public/site-header.tsx");
|
||||
|
||||
expect(header).toContain("<Suspense fallback={null}>");
|
||||
expect(header).toContain("<AdminEditLink");
|
||||
});
|
||||
|
||||
it("preloads only the first directory cover", async () => {
|
||||
const directory = await repositoryFile(
|
||||
"components/public/structured-directory.tsx",
|
||||
);
|
||||
|
||||
expect(directory).toContain("preload={index === 0}");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user