feat(guides) : add safer publishing and discovery
CI / Verify (push) Successful in 1m35s
CI / Build immutable images and deploy (push) Successful in 5m27s

This commit is contained in:
2026-09-19 13:32:57 +07:00 Unverified
parent 6698dc1302
commit 9896e8a4ad
27 changed files with 3213 additions and 108 deletions
+3
View File
@@ -84,6 +84,9 @@ export default async function StructuredEditorPage({
</h1>
<EditorPresence guideId={guide.id} section={sectionTitle} />
</div>
<Button className="ml-auto" variant="outline" render={<Link href={`/admin/${guide.characterKey}/preview/${page}`} target="_blank" />} nativeButton={false}>
Preview
</Button>
</header>
<GuideSectionNav
guideId={guide.id}
+4 -1
View File
@@ -51,7 +51,10 @@ export default async function GuideOverviewPage({ params }: { params: Promise<{
<h1 className="font-heading text-3xl font-semibold">{guide.name}</h1>
<EditorPresence guideId={guide.id} section="Overview" />
</div>
<GuideVisibilityControl guideId={guide.id} guideName={guide.name} isPublic={guide.isPublic} />
<GuideVisibilityControl guideId={guide.id} guideName={guide.name} isPublic={guide.isPublic} version={guide.version} />
<Button variant="outline" render={<Link href={`/admin/${guide.characterKey}/preview`} target="_blank" />} nativeButton={false}>
Preview
</Button>
</header>
<GuideSectionNav guideId={guide.id} characterKey={guide.characterKey} current="overview" extras={extras} />
<GuideOverviewForm key={guide.version} guide={guide} />
@@ -0,0 +1,27 @@
import { notFound, redirect } from "next/navigation";
import { connection } from "next/server";
import { StructuredGuideShell } from "@/components/public/structured-guide";
import { getAdminSession } from "@/lib/auth/server";
import { publicGuideSections } from "@/lib/guides/public-sections";
import { getAdminGuideContent, getGuideEditorHeader } from "@/lib/guides/queries";
export const instant = false;
export default async function GuidePreviewPage({
params,
}: {
params: Promise<{ character: string; page?: string[] }>;
}) {
await connection();
if (!(await getAdminSession())) redirect("/admin/login");
const { character, page } = await params;
if (page && page.length > 1) notFound();
const header = await getGuideEditorHeader(character);
if (!header) notFound();
const guide = await getAdminGuideContent(header.slug);
if (!guide) notFound();
const section = page?.[0];
if (section && !publicGuideSections(guide).some((item) => item.slug === section)) notFound();
return <StructuredGuideShell guide={guide} section={section} preview />;
}
+36 -10
View File
@@ -9,6 +9,10 @@ import {
deleteGlossaryAlias,
} from "@/lib/glossary-repository";
import { requireAdmin } from "@/lib/auth/server";
import { getCurrentCatalogVersion } from "@/lib/catalog/sync";
import { publicGuideSections } from "@/lib/guides/public-sections";
import { publicationIssues } from "@/lib/guides/publication";
import { getAdminGuideContentById } from "@/lib/guides/queries";
import {
createExtraSection,
createGuide,
@@ -20,7 +24,6 @@ import {
saveArtifacts,
saveConstellations,
saveExtraSection,
saveExtraSections,
saveTeams,
setGuideVisibility,
saveWeapons,
@@ -126,13 +129,6 @@ export async function loadTeamImportGuide(guideId: string) {
}
}
export async function saveExtraSectionList(guideId: string, input: unknown) {
await requireAdmin();
const result = await runGuideAction(() => saveExtraSections(guideId, input));
if (result.status === "saved") updateTag(pageCacheTag(guideId));
return result;
}
export async function createGuideExtra(guideId: string, input: unknown) {
await requireAdmin();
try {
@@ -204,10 +200,40 @@ export async function restoreStructuredGuide(guideId: string) {
export async function setStructuredGuideVisibility(
guideId: string,
isPublic: boolean,
input: unknown,
) {
await requireAdmin();
return runGuideAction(() => setGuideVisibility(guideId, isPublic));
const parsed = z.strictObject({
isPublic: z.boolean(),
expectedVersion: z.number().int().positive(),
publicationNote: z.string().trim().max(500).optional(),
}).safeParse(input);
if (!parsed.success) return { status: "error" as const, message: "ข้อมูลการเผยแพร่ไม่ถูกต้อง" };
if (!parsed.data.isPublic) {
return runGuideAction(() => setGuideVisibility(guideId, parsed.data));
}
const [guide, catalogVersion] = await Promise.all([
getAdminGuideContentById(guideId),
getCurrentCatalogVersion(),
]);
const issues = publicationIssues({
exists: Boolean(guide),
overview: guide?.overview ?? "",
coverReady: guide?.coverStatus === "ready",
catalogVersion,
renderableSectionCount: guide ? publicGuideSections(guide).length : 0,
version: guide?.version ?? 0,
expectedVersion: parsed.data.expectedVersion,
});
if (issues.length || !catalogVersion) {
return { status: "error" as const, message: issues.join(" • "), issues };
}
return runGuideAction(() => setGuideVisibility(guideId, {
...parsed.data,
publicationNote: parsed.data.publicationNote || null,
catalogVersion,
}));
}
export async function deleteStructuredGuide(guideId: string) {