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
10 changes: 10 additions & 0 deletions src/app/builders/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { fetchBuilder } from "@/lib/api/builders";
import { DraftPreviewBanner } from "@/components/auth/DraftPreviewBanner";
import { isDraft, primeAdminPreviewToken } from "@/lib/preview";

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
// Prime the request-scoped token so draft metadata resolves for admins.
await primeAdminPreviewToken();
try {
const builder = await fetchBuilder(slug);
return {
Expand Down Expand Up @@ -37,6 +41,9 @@ export default async function BuilderPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;

const accessToken = await primeAdminPreviewToken();

let builder;
try {
builder = await fetchBuilder(slug);
Expand All @@ -50,6 +57,9 @@ export default async function BuilderPage({

return (
<div className="mx-[10px] my-[10px] border border-border-light bg-bg">
{accessToken && isDraft(builder.publishedAt) && (
<DraftPreviewBanner state="viewing-draft" slug={slug} />
)}
<article className="animate-fade-in max-w-2xl mx-auto px-5 pt-[50px] pb-[60px]">
<Link
href="/builders"
Expand Down
38 changes: 7 additions & 31 deletions src/app/memos/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,9 @@ import { buildGraph } from "@/lib/schemas/graph";
import { generateArticleSchema } from "@/lib/schemas/generators/article";
import { generateBreadcrumbSchema } from "@/lib/schemas/generators/breadcrumb";
import { generateOrganizationSchema } from "@/lib/schemas/generators/organization";
import { DraftPreviewBanner } from "./DraftPreviewBanner";
import { setAccessToken } from "@/lib/auth-token";
import { getCurrentUser, getAccessTokenCookie } from "@/lib/auth";

// Draft preview is gated on the signed-in user actually being an admin (live
// from /me), never on a baked cookie. When they are, we hand apiFetch the
// access token so it fetches drafts; otherwise the request store stays empty
// and only published content is returned.
async function resolveAccessToken(): Promise<string | undefined> {
const user = await getCurrentUser();
const token = user?.admin ? await getAccessTokenCookie() : undefined;
setAccessToken(token);
return token;
}
import { DraftPreviewBanner } from "@/components/auth/DraftPreviewBanner";
import { primeAdminPreviewToken } from "@/lib/preview";
import { getCurrentUser } from "@/lib/auth";

export async function generateStaticParams() {
try {
Expand All @@ -42,7 +31,7 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { slug } = await params;
// Prime the request-scoped token so draft metadata resolves for admins.
await resolveAccessToken();
await primeAdminPreviewToken();
let memo;
try {
memo = await fetchMemo(slug);
Expand Down Expand Up @@ -85,29 +74,16 @@ export default async function MemoDetailPage({
}) {
const { slug } = await params;

const accessToken = await resolveAccessToken();
// Cached per-request alongside resolveAccessToken's call — drives the
const accessToken = await primeAdminPreviewToken();
// Cached per-request alongside primeAdminPreviewToken's call — drives the
// engagement UI's signed-in / postal-code-ready states.
const viewer = await getCurrentUser();

let memo;
try {
memo = await fetchMemo(slug);
} catch {
if (!accessToken) notFound();

return (
<div className="mx-[10px] my-[10px] border border-border-light bg-bg">
<div className="max-w-[720px] mx-auto px-[5vw] md:px-[10vw] py-24 flex flex-col items-center gap-8 text-center">
<p className="type-label text-text-secondary">404</p>
<h1 className="type-title">Memo not found</h1>
<p className="type-body text-text-secondary">
This memo doesn&apos;t exist or hasn&apos;t been published yet.
</p>
<DraftPreviewBanner state="draft-not-found" slug={slug} />
</div>
</div>
);
notFound();
}

if (memo.slug !== slug) {
Expand Down
10 changes: 10 additions & 0 deletions src/app/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { SubscribeButton } from "@/components/ui/subscribe-button";
import { buildGraph } from "@/lib/schemas/graph";
import { generateBreadcrumbSchema } from "@/lib/schemas/generators/breadcrumb";
import { generateOrganizationSchema } from "@/lib/schemas/generators/organization";
import { DraftPreviewBanner } from "@/components/auth/DraftPreviewBanner";
import { isDraft, primeAdminPreviewToken } from "@/lib/preview";

export async function generateStaticParams() {
try {
Expand All @@ -25,6 +27,8 @@ export async function generateMetadata({
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
// Prime the request-scoped token so draft metadata resolves for admins.
await primeAdminPreviewToken();
let post;
try {
post = await fetchPost(slug);
Expand Down Expand Up @@ -65,6 +69,9 @@ export default async function PostDetailPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;

const accessToken = await primeAdminPreviewToken();

let post;
try {
post = await fetchPost(slug);
Expand Down Expand Up @@ -102,6 +109,9 @@ export default async function PostDetailPage({

return (
<div className="mx-[10px] my-[10px] border border-border-light bg-bg">
{accessToken && isDraft(post.publishedAt) && (
<DraftPreviewBanner state="viewing-draft" slug={slug} />
)}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
Expand Down
3 changes: 3 additions & 0 deletions src/lib/api/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface YFBuilder {
interface YFBuilderDetail extends YFBuilder {
body: string | null;
author: string | null;
published_at: string | null;
}

export interface BuilderSerialized {
Expand All @@ -27,6 +28,7 @@ export interface BuilderSerialized {
export interface BuilderDetailSerialized extends BuilderSerialized {
body: string | null;
author: string | null;
publishedAt: string | null;
}

function stripHtml(html: string | null): string | null {
Expand Down Expand Up @@ -68,5 +70,6 @@ export async function fetchBuilder(slug: string): Promise<BuilderDetailSerialize
...mapBuilder(b),
body: b.body,
author: stripHtml(b.author),
publishedAt: b.published_at,
};
}
21 changes: 21 additions & 0 deletions src/lib/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { getAccessTokenCookie, getCurrentUser } from "@/lib/auth";
import { setAccessToken } from "@/lib/auth-token";

// Draft preview is gated on the signed-in user actually being an admin (live
// from /me), never on a baked cookie. When they are, we hand apiFetch the
// access token so it fetches drafts; otherwise the request store stays empty
// and only published content is returned. Returns the token so callers can
// branch on "preview mode" (e.g. render a draft banner or the admin 404).
export async function primeAdminPreviewToken(): Promise<string | undefined> {
const user = await getCurrentUser();
const token = user?.admin ? await getAccessTokenCookie() : undefined;
setAccessToken(token);
return token;
}

// A record is a draft when it has no publish date, or one scheduled in the
// future. The preview banner is for genuine drafts only — not every record an
// admin happens to be viewing.
export function isDraft(publishedAt: string | null | undefined): boolean {
return !publishedAt || new Date(publishedAt) > new Date();
}
Loading