Skip to content

Commit a9d479b

Browse files
Improvements for website-next Pt. 2 (#9773)
1 parent b62ae44 commit a9d479b

64 files changed

Lines changed: 2413 additions & 372 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

website-next/.storybook/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import type { StorybookConfig } from "@storybook/nextjs-vite";
22

33
const config: StorybookConfig = {
4-
stories: ["../src/design-system/**/*.stories.@(ts|tsx|mdx)"],
4+
stories: [
5+
"../src/design-system/**/*.stories.@(ts|tsx|mdx)",
6+
"../src/components/**/*.stories.@(ts|tsx|mdx)",
7+
],
58
framework: {
69
name: "@storybook/nextjs-vite",
710
options: {},

website-next/.storybook/preview.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const preview: Preview = {
1010
date: /Date$/i,
1111
},
1212
},
13+
options: {
14+
storySort: {
15+
method: "alphabetical",
16+
},
17+
},
1318
},
1419
};
1520

website-next/TODOS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
- Re-add promotional pages
44
- Re-add home page
55
- Image optimizations
6-
- Blog/Tag explore pages
76
- Switch to proper theme
7+
- Tracking
Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import Link from "next/link";
2+
import { FromOurBlog } from "@/src/components/FromOurBlog";
23

34
export default function Home() {
45
return (
5-
<>
6-
<h2>Docs</h2>
7-
<ul>
8-
<li>
9-
<Link href="/docs/example/getting-started">Example</Link>
10-
</li>
11-
</ul>
12-
</>
6+
<div className="px-5 py-8 sm:px-12">
7+
<div className="mx-auto flex max-w-6xl flex-col gap-12">
8+
<section>
9+
<h2>Docs</h2>
10+
<ul>
11+
<li>
12+
<Link href="/docs/example/getting-started">Example</Link>
13+
</li>
14+
</ul>
15+
</section>
16+
17+
<FromOurBlog />
18+
</div>
19+
</div>
1320
);
1421
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import fs from "node:fs/promises";
2+
import path from "node:path";
3+
import type { Metadata } from "next";
4+
import { notFound } from "next/navigation";
5+
import { BlogMetadata } from "@/src/components/BlogMetadata";
6+
import { BlogTags } from "@/src/components/BlogTags";
7+
import { BlogTeaserGrid } from "@/src/components/BlogTeaserGrid";
8+
import { Pagination } from "@/src/design-system/Pagination";
9+
import { SimilarPosts } from "@/src/components/SimilarPosts";
10+
import { Typography } from "@/src/design-system/Typography";
11+
import { paginate, POSTS_PER_PAGE } from "@/src/helpers/blogPaging";
12+
import {
13+
BLOG_ROOT,
14+
listBlogPosts,
15+
resolveBlogFile,
16+
} from "@/src/helpers/blogPaths";
17+
import { findSimilarPosts, listBlogPostSummaries } from "@/src/helpers/blogPosts";
18+
import { compileDoc } from "@/src/helpers/compileDoc";
19+
import { readFrontmatter } from "@/src/helpers/readFrontmatter";
20+
import { estimateReadingTime } from "@/src/helpers/readingTime";
21+
22+
type BlogFrontmatter = {
23+
title?: string;
24+
description?: string;
25+
author?: string;
26+
authorUrl?: string;
27+
authorImageUrl?: string;
28+
date?: string;
29+
tags?: string[];
30+
};
31+
32+
type Params = { slug: string[] };
33+
type PageProps = { params: Promise<Params> };
34+
35+
export const dynamicParams = false;
36+
37+
export function generateStaticParams(): Params[] {
38+
const postParams = listBlogPosts().map<Params>(({ parsed }) => ({
39+
slug: [parsed.year, parsed.month, parsed.day, parsed.slug],
40+
}));
41+
42+
const summaries = listBlogPostSummaries();
43+
const totalPages = Math.max(
44+
1,
45+
Math.ceil(summaries.length / POSTS_PER_PAGE),
46+
);
47+
const pageParams: Params[] = [];
48+
for (let p = 2; p <= totalPages; p++) {
49+
pageParams.push({ slug: [String(p)] });
50+
}
51+
52+
const params = [...postParams, ...pageParams];
53+
// output: export requires at least one prerendered path; placeholder
54+
// renders 404 via notFound() when no content is present.
55+
return params.length > 0 ? params : [{ slug: ["__empty__"] }];
56+
}
57+
58+
export async function generateMetadata({
59+
params,
60+
}: PageProps): Promise<Metadata> {
61+
const { slug } = await params;
62+
if (isPaginationSlug(slug)) {
63+
return { title: "Blog" };
64+
}
65+
const rel = resolveBlogFile(slug);
66+
if (rel === null) {
67+
return {};
68+
}
69+
const { title, description } = readFrontmatter(path.join(BLOG_ROOT, rel));
70+
return {
71+
title,
72+
description,
73+
};
74+
}
75+
76+
export default async function BlogSlugPage({ params }: PageProps) {
77+
const { slug } = await params;
78+
79+
if (isPaginationSlug(slug)) {
80+
return renderPagination(Number(slug[0]));
81+
}
82+
83+
const rel = resolveBlogFile(slug);
84+
if (rel === null) {
85+
notFound();
86+
}
87+
88+
const absPath = path.join(BLOG_ROOT, rel);
89+
const [{ content, frontmatter }, raw] = await Promise.all([
90+
compileDoc<BlogFrontmatter>(absPath),
91+
fs.readFile(absPath, "utf-8"),
92+
]);
93+
const readingTime = estimateReadingTime(raw).text;
94+
95+
const summaries = listBlogPostSummaries();
96+
const stem = `${slug[0]}-${slug[1]}-${slug[2]}-${slug.slice(3).join("/")}`;
97+
const current = summaries.find((s) => s.stem === stem);
98+
const similar = current ? findSimilarPosts(current, summaries) : [];
99+
const featuredImage = current?.featuredImage ?? null;
100+
101+
return (
102+
<main className="px-5 py-8 sm:px-12">
103+
<article className="mx-auto max-w-5xl">
104+
{featuredImage ? (
105+
// eslint-disable-next-line @next/next/no-img-element
106+
<img
107+
src={featuredImage}
108+
alt=""
109+
loading="eager"
110+
decoding="async"
111+
className="mb-6 aspect-[16/9] w-full rounded-lg object-cover"
112+
/>
113+
) : null}
114+
{frontmatter.title ? (
115+
<Typography variant="h1">{frontmatter.title}</Typography>
116+
) : null}
117+
<BlogMetadata
118+
author={frontmatter.author}
119+
authorUrl={frontmatter.authorUrl}
120+
authorImageUrl={frontmatter.authorImageUrl}
121+
date={frontmatter.date}
122+
readingTime={readingTime}
123+
/>
124+
<BlogTags tags={frontmatter.tags} />
125+
{content}
126+
<SimilarPosts posts={similar} />
127+
</article>
128+
</main>
129+
);
130+
}
131+
132+
function isPaginationSlug(slug: string[]): boolean {
133+
return slug.length === 1 && /^\d+$/.test(slug[0]);
134+
}
135+
136+
function renderPagination(pageNum: number) {
137+
if (!Number.isInteger(pageNum) || pageNum < 2) {
138+
notFound();
139+
}
140+
const slice = paginate(listBlogPostSummaries(), pageNum);
141+
if (slice === null) {
142+
notFound();
143+
}
144+
145+
return (
146+
<div className="px-5 py-8 sm:px-12">
147+
<div className="mx-auto flex max-w-6xl flex-col gap-6">
148+
<Typography variant="h1">Blog</Typography>
149+
<BlogTeaserGrid posts={slice.posts} />
150+
<Pagination
151+
currentPage={slice.currentPage}
152+
totalPages={slice.totalPages}
153+
hrefForPage={(p) => (p === 1 ? "/blog" : `/blog/${p}`)}
154+
/>
155+
</div>
156+
</div>
157+
);
158+
}

website-next/app/blog/page.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Pagination } from "@/src/design-system/Pagination";
2+
import { BlogTeaserGrid } from "@/src/components/BlogTeaserGrid";
3+
import { Typography } from "@/src/design-system/Typography";
4+
import { paginate } from "@/src/helpers/blogPaging";
5+
import { listBlogPostSummaries } from "@/src/helpers/blogPosts";
6+
7+
export const metadata = {
8+
title: "Blog",
9+
description: "The ChilliCream blog: announcements, deep dives, and how-tos.",
10+
};
11+
12+
export default function BlogsIndex() {
13+
const posts = listBlogPostSummaries();
14+
const slice = paginate(posts, 1);
15+
if (slice === null) {
16+
return (
17+
<div className="px-5 py-8 sm:px-12">
18+
<div className="mx-auto flex max-w-6xl flex-col gap-6">
19+
<Typography variant="h1">Blog</Typography>
20+
<BlogTeaserGrid posts={[]} />
21+
</div>
22+
</div>
23+
);
24+
}
25+
26+
return (
27+
<div className="px-5 py-8 sm:px-12">
28+
<div className="mx-auto flex max-w-6xl flex-col gap-6">
29+
<Typography variant="h1">Blog</Typography>
30+
<BlogTeaserGrid posts={slice.posts} />
31+
<Pagination
32+
currentPage={slice.currentPage}
33+
totalPages={slice.totalPages}
34+
hrefForPage={(p) => (p === 1 ? "/blog" : `/blog/${p}`)}
35+
/>
36+
</div>
37+
</div>
38+
);
39+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { listBlogPostSummaries } from "@/src/helpers/blogPosts";
2+
import { SITE_URL } from "@/src/helpers/siteUrl";
3+
4+
export const dynamic = "force-static";
5+
6+
const FEED_TITLE = "ChilliCream Blog";
7+
const FEED_DESCRIPTION =
8+
"Announcements, deep dives, and how-tos from the ChilliCream GraphQL Platform team.";
9+
10+
function escape(text: string): string {
11+
return text
12+
.replace(/&/g, "&amp;")
13+
.replace(/</g, "&lt;")
14+
.replace(/>/g, "&gt;")
15+
.replace(/"/g, "&quot;")
16+
.replace(/'/g, "&apos;");
17+
}
18+
19+
function asRfc822(iso: string): string {
20+
const d = new Date(iso);
21+
if (Number.isNaN(d.getTime())) {
22+
return new Date().toUTCString();
23+
}
24+
return d.toUTCString();
25+
}
26+
27+
export function GET() {
28+
const posts = listBlogPostSummaries();
29+
const buildDate = posts[0]?.date ?? new Date().toISOString();
30+
31+
const items = posts
32+
.map((post) => {
33+
const url = `${SITE_URL}${post.href}`;
34+
const description = post.description ?? "";
35+
const enclosure = post.featuredImage
36+
? `<enclosure url="${SITE_URL}${escape(post.featuredImage)}" type="image/png" />`
37+
: "";
38+
const categories = post.tags
39+
.map((tag) => `<category>${escape(tag)}</category>`)
40+
.join("");
41+
return ` <item>
42+
<title>${escape(post.title)}</title>
43+
<link>${escape(url)}</link>
44+
<guid isPermaLink="true">${escape(url)}</guid>
45+
<pubDate>${asRfc822(post.date)}</pubDate>
46+
${post.author ? `<author>${escape(post.author)}</author>` : ""}
47+
${categories}
48+
${enclosure}
49+
<description>${escape(description)}</description>
50+
</item>`;
51+
})
52+
.join("\n");
53+
54+
const body = `<?xml version="1.0" encoding="UTF-8"?>
55+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
56+
<channel>
57+
<title>${escape(FEED_TITLE)}</title>
58+
<link>${SITE_URL}/blog</link>
59+
<atom:link href="${SITE_URL}/blog/rss.xml" rel="self" type="application/rss+xml" />
60+
<description>${escape(FEED_DESCRIPTION)}</description>
61+
<language>en-us</language>
62+
<lastBuildDate>${asRfc822(buildDate)}</lastBuildDate>
63+
${items}
64+
</channel>
65+
</rss>
66+
`;
67+
68+
return new Response(body, {
69+
headers: {
70+
"Content-Type": "application/rss+xml; charset=utf-8",
71+
},
72+
});
73+
}

0 commit comments

Comments
 (0)