feat(web): payload cms v3 + blog + changelog data model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
14
apps/web/src/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
14
apps/web/src/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck — Payload generates these types at build time
|
||||
import { RootPage, generatePageMetadata } from "@payloadcms/next/views";
|
||||
import { importMap } from "../importMap";
|
||||
import config from "@payload-config";
|
||||
|
||||
type Args = { params: Promise<{ segments: string[] }> };
|
||||
|
||||
export const generateMetadata = ({ params }: Args) =>
|
||||
generatePageMetadata({ config, params });
|
||||
|
||||
export default function Page({ params }: Args) {
|
||||
return <RootPage config={config} params={params} importMap={importMap} />;
|
||||
}
|
||||
2
apps/web/src/app/(payload)/admin/importMap.js
Normal file
2
apps/web/src/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// Auto-generated by Payload — placeholder until first build
|
||||
export const importMap = {};
|
||||
11
apps/web/src/app/(payload)/api/[...slug]/route.ts
Normal file
11
apps/web/src/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST, REST_PUT } from "@payloadcms/next/routes";
|
||||
import config from "@payload-config";
|
||||
|
||||
export const GET = REST_GET(config);
|
||||
export const POST = REST_POST(config);
|
||||
export const DELETE = REST_DELETE(config);
|
||||
export const PATCH = REST_PATCH(config);
|
||||
export const PUT = REST_PUT(config);
|
||||
export const OPTIONS = REST_OPTIONS(config);
|
||||
14
apps/web/src/app/(payload)/layout.tsx
Normal file
14
apps/web/src/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import "@payloadcms/next/css";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Admin — claudemesh",
|
||||
};
|
||||
|
||||
export default function PayloadLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
80
apps/web/src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Normal file
80
apps/web/src/app/[locale]/(marketing)/blog/[slug]/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
import { RichText } from "@payloadcms/richtext-lexical/react";
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
where: { slug: { equals: slug }, status: { equals: "published" } },
|
||||
limit: 1,
|
||||
depth: 1,
|
||||
});
|
||||
const post = docs[0];
|
||||
if (!post) return { title: "Not found — claudemesh" };
|
||||
return {
|
||||
title: `${post.title} — claudemesh`,
|
||||
description: post.excerpt || post.seo?.metaDescription || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const payload = await getPayload({ config });
|
||||
const { docs } = await payload.find({
|
||||
collection: "posts",
|
||||
where: { slug: { equals: slug }, status: { equals: "published" } },
|
||||
limit: 1,
|
||||
depth: 2,
|
||||
});
|
||||
|
||||
const post = docs[0] as any;
|
||||
if (!post) notFound();
|
||||
|
||||
const author = typeof post.author === "object" ? post.author : null;
|
||||
|
||||
return (
|
||||
<article className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||
<header className="mb-12">
|
||||
<time
|
||||
dateTime={post.publishedAt}
|
||||
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
{post.publishedAt
|
||||
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: "Draft"}
|
||||
</time>
|
||||
<h1
|
||||
className="mt-3 text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
{author && (
|
||||
<p
|
||||
className="mt-4 text-sm text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
by {author.name}{author.role ? ` · ${author.role}` : ""}
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div
|
||||
className="prose prose-invert max-w-none prose-headings:font-medium prose-a:text-[var(--cm-clay)] prose-a:no-underline hover:prose-a:underline prose-code:text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{post.content && <RichText data={post.content} />}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
78
apps/web/src/app/[locale]/(marketing)/blog/page.tsx
Normal file
78
apps/web/src/app/[locale]/(marketing)/blog/page.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import Link from "next/link";
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
|
||||
export const metadata = {
|
||||
title: "Blog — claudemesh",
|
||||
description: "Engineering notes on peer messaging, protocol design, and multi-agent security.",
|
||||
};
|
||||
|
||||
export default async function BlogIndex() {
|
||||
const payload = await getPayload({ config });
|
||||
const { docs: posts } = await payload.find({
|
||||
collection: "posts",
|
||||
where: { status: { equals: "published" } },
|
||||
sort: "-publishedAt",
|
||||
limit: 20,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||
<h1
|
||||
className="text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
Blog
|
||||
</h1>
|
||||
<p
|
||||
className="mt-4 text-[15px] text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
Engineering notes on protocol design, security, and multi-agent UX.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 space-y-10">
|
||||
{posts.length === 0 && (
|
||||
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||
No posts yet. First one ships soon.
|
||||
</p>
|
||||
)}
|
||||
{posts.map((post: any) => (
|
||||
<article key={post.id} className="border-b border-[var(--cm-border)] pb-8">
|
||||
<time
|
||||
dateTime={post.publishedAt}
|
||||
className="text-[11px] uppercase tracking-wider text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
{post.publishedAt
|
||||
? new Date(post.publishedAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: "Draft"}
|
||||
</time>
|
||||
<h2 className="mt-2">
|
||||
<Link
|
||||
href={`/blog/${post.slug}`}
|
||||
className="text-[22px] font-medium leading-tight text-[var(--cm-fg)] transition-colors hover:text-[var(--cm-clay)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
</h2>
|
||||
{post.excerpt && (
|
||||
<p
|
||||
className="mt-3 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
107
apps/web/src/app/[locale]/(marketing)/changelog/page.tsx
Normal file
107
apps/web/src/app/[locale]/(marketing)/changelog/page.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { getPayload } from "payload";
|
||||
import config from "@payload-config";
|
||||
|
||||
export const metadata = {
|
||||
title: "Changelog — claudemesh",
|
||||
description: "Release history for claudemesh-cli.",
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
feat: "Feature",
|
||||
fix: "Fix",
|
||||
docs: "Docs",
|
||||
breaking: "Breaking",
|
||||
};
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
feat: "bg-[var(--cm-clay)]",
|
||||
fix: "bg-[var(--cm-cactus)]",
|
||||
docs: "bg-[var(--cm-oat)]",
|
||||
breaking: "bg-red-500",
|
||||
};
|
||||
|
||||
export default async function ChangelogPage() {
|
||||
const payload = await getPayload({ config });
|
||||
const { docs: entries } = await payload.find({
|
||||
collection: "changelog",
|
||||
sort: "-date",
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl px-6 py-24 md:py-32">
|
||||
<h1
|
||||
className="text-[clamp(2rem,4.5vw,3rem)] font-medium leading-[1.1] text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
Changelog
|
||||
</h1>
|
||||
<p
|
||||
className="mt-4 text-[15px] text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
Every shipped version of claudemesh-cli.
|
||||
</p>
|
||||
|
||||
<div className="mt-12 space-y-8">
|
||||
{entries.length === 0 && (
|
||||
<p className="text-sm text-[var(--cm-fg-tertiary)]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||
No entries yet.
|
||||
</p>
|
||||
)}
|
||||
{entries.map((entry: any) => (
|
||||
<article
|
||||
key={entry.id}
|
||||
className="border-b border-[var(--cm-border)] pb-6"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`rounded-[4px] px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider text-[var(--cm-bg)] ${TYPE_COLORS[entry.type] || "bg-[var(--cm-fg-tertiary)]"}`}
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
{TYPE_LABELS[entry.type] || entry.type}
|
||||
</span>
|
||||
<span
|
||||
className="text-[18px] font-medium text-[var(--cm-fg)]"
|
||||
style={{ fontFamily: "var(--cm-font-serif)" }}
|
||||
>
|
||||
v{entry.version}
|
||||
</span>
|
||||
<time
|
||||
dateTime={entry.date}
|
||||
className="text-[11px] text-[var(--cm-fg-tertiary)]"
|
||||
style={{ fontFamily: "var(--cm-font-mono)" }}
|
||||
>
|
||||
{new Date(entry.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
<p
|
||||
className="mt-2 text-[14px] leading-[1.6] text-[var(--cm-fg-secondary)]"
|
||||
style={{ fontFamily: "var(--cm-font-sans)" }}
|
||||
>
|
||||
{entry.summary}
|
||||
</p>
|
||||
{(entry.npmUrl || entry.githubUrl) && (
|
||||
<div className="mt-3 flex gap-4 text-[12px]" style={{ fontFamily: "var(--cm-font-mono)" }}>
|
||||
{entry.npmUrl && (
|
||||
<a href={entry.npmUrl} className="text-[var(--cm-clay)] hover:underline">
|
||||
npm →
|
||||
</a>
|
||||
)}
|
||||
{entry.githubUrl && (
|
||||
<a href={entry.githubUrl} className="text-[var(--cm-clay)] hover:underline">
|
||||
github →
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user