Skip to content

Commit 1cebf33

Browse files
authored
Add blog (#5074)
1 parent a227cb3 commit 1cebf33

File tree

12 files changed

+218
-28
lines changed

12 files changed

+218
-28
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"vitest": "^2.1.9",
4747
"zod": "workspace:*",
4848
"zod3": "npm:zod@~3.24.0",
49-
"zshy": "^0.3.3"
49+
"zshy": "^0.3.5"
5050
},
5151
"lint-staged": {
5252
"packages/*/src/**/*.ts": [

packages/docs/app/_home/page.tsx

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { blog } from "@/loaders/source";
2+
import defaultMdxComponents from "fumadocs-ui/mdx";
3+
import Link from "next/link";
4+
import { notFound } from "next/navigation";
5+
import type { ComponentType } from "react";
6+
7+
export const revalidate = 3600;
8+
9+
export default async function Page(props: { params: Promise<{ slug: string }> }) {
10+
const params = await props.params;
11+
const page = blog.getPage([params.slug]) as any;
12+
13+
if (!page) notFound();
14+
const Mdx = page.data.body as ComponentType<any>;
15+
const author: string = (page.data?.author as string | undefined) ?? "Colin McDonnell";
16+
const dateValue = page.data?.date as string | Date | undefined;
17+
const formattedDate = dateValue
18+
? new Date(dateValue).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
19+
: undefined;
20+
21+
return (
22+
<>
23+
<header className="container px-4">
24+
<div className="mx-auto max-w-3xl py-10 md:py-14">
25+
{page.data.title ? (
26+
<>
27+
<nav className="mb-0">
28+
<Link href="/blog" className="text-base text-fd-muted-foreground hover:text-fd-foreground">
29+
Blog /
30+
</Link>
31+
</nav>
32+
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight leading-tight mb-3">
33+
{page.data.title}
34+
</h1>
35+
</>
36+
) : null}
37+
{/* {page.data.description ? (
38+
<p className="text-lg text-fd-muted-foreground mb-4">{page.data.description}</p>
39+
) : null} */}
40+
<div className="flex flex-wrap items-center gap-2 text-sm text-fd-muted-foreground">
41+
<a href="https://x.com/colinhacks" className="font-medium underline underline-offset-2">
42+
{author}
43+
</a>
44+
{formattedDate ? <span className="opacity-60"></span> : null}
45+
{formattedDate ? <time dateTime={new Date(dateValue as any).toISOString()}>{formattedDate}</time> : null}
46+
</div>
47+
</div>
48+
</header>
49+
<article className="container px-4">
50+
<div className="mx-auto max-w-3xl flex flex-col py-6 md:py-10">
51+
<div className="prose prose-lg min-w-0">
52+
<Mdx components={defaultMdxComponents} />
53+
</div>
54+
<div className="mt-10 pt-6 border-t text-sm text-fd-muted-foreground">
55+
<p>
56+
Written by{" "}
57+
<a href="https://x.com/colinhacks" className="font-medium underline underline-offset-2">
58+
{author}
59+
</a>
60+
{formattedDate ? (
61+
<>
62+
{" "}
63+
on <time dateTime={new Date(dateValue as any).toISOString()}>{formattedDate}</time>
64+
</>
65+
) : null}
66+
</p>
67+
</div>
68+
</div>
69+
</article>
70+
</>
71+
);
72+
}
73+
74+
export function generateStaticParams(): { slug: string }[] {
75+
return blog.getPages().map((page) => ({
76+
slug: page.slugs[0],
77+
}));
78+
}
79+
80+
export async function generateMetadata(props: { params: Promise<{ slug: string }> }) {
81+
const params = await props.params;
82+
const page = blog.getPage([params.slug]) as any;
83+
84+
if (!page) notFound();
85+
86+
return {
87+
title: (page.data?.title as string | undefined) ?? params.slug,
88+
description: (page.data?.description as string | undefined) ?? undefined,
89+
};
90+
}

packages/docs/app/_home/layout.tsx renamed to packages/docs/app/blog/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { baseOptions } from "@/app/layout.config";
22
import { HomeLayout } from "fumadocs-ui/layouts/home";
33
import type { ReactNode } from "react";
44

5-
export default function Layout({ children }: { children: ReactNode }) {
5+
export default function BlogLayout({ children }: { children: ReactNode }) {
66
return <HomeLayout {...baseOptions}>{children}</HomeLayout>;
77
}

packages/docs/app/blog/page.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { blog } from "@/loaders/source";
2+
import Link from "next/link";
3+
4+
export const revalidate = 3600;
5+
6+
export default function BlogIndexPage() {
7+
const posts = blog.getPages() as any[];
8+
9+
return (
10+
<main className="grow container mx-auto px-4 py-12">
11+
<div className="mx-auto max-w-3xl">
12+
<h1 className="text-4xl md:text-5xl font-extrabold tracking-tight mb-6">Blog</h1>
13+
<p className="text-fd-muted-foreground mb-10">Updates, announcements, and deep dives from the Zod project.</p>
14+
</div>
15+
<div className="mx-auto max-w-3xl grid gap-6">
16+
{posts.map((post) => {
17+
const title = post.data.title ?? post.slugs.join("/") ?? "Untitled";
18+
const description = post.data.description ?? "";
19+
const dateValue = post.data?.date as string | Date | undefined;
20+
const formattedDate = dateValue
21+
? new Date(dateValue).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
22+
: undefined;
23+
return (
24+
<Link
25+
key={post.url}
26+
href={post.url}
27+
className="block rounded-xl border p-6 hover:shadow-md transition-shadow bg-fd-secondary/30"
28+
>
29+
<h2 className="text-2xl font-semibold mb-2">{title}</h2>
30+
{description ? <p className="mb-2 text-fd-muted-foreground">{description}</p> : null}
31+
{formattedDate ? <p className="text-sm text-fd-muted-foreground">{formattedDate}</p> : null}
32+
</Link>
33+
);
34+
})}
35+
</div>
36+
</main>
37+
);
38+
}

packages/docs/app/layout.config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Logo from "@/public/logo/logo.png";
33
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
44
import Image from "next/image";
55
export const logo = (
6-
<div className="md:mb-1 md:h-7">
6+
<div className="md:mb-0 md:h-7">
77
<Image alt="Zod logo" src={LogoWhite} sizes="100px" className="hidden dark:block w-8" aria-label="Zod logo" />
88
<Image alt="Zod logo" src={Logo} sizes="100px" className="block dark:hidden w-8" aria-label="Zod logo" />
99
</div>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: Joining Clerk as an OSS Fellow to work on Zod 4
3+
author: Colin McDonnell
4+
date: 2024-06-11
5+
description: Announcing my Clerk OSS Fellowship and what's coming in Zod 4.
6+
---
7+
8+
I'm thrilled to announce that I'm the inaugural recipient of [Clerk's](https://go.clerk.com/zod-clerk) OSS Fellowship! This fellowship is kind of like a "summer internship"—Clerk is paying me a full-time wage (think entry-level software engineer) to work on Zod full-time throughout summer 2024.
9+
10+
In the context of both my own career path and Zod's development, this is a perfect arrangement, and I'm beyond grateful that Clerk was willing to experiment with some alternative funding arrangements for OSS.
11+
12+
Let's look at some of the context here.
13+
14+
## On deck: Zod 4
15+
16+
The current major version of Zod (v3) was released in 2021. In terms of structure and implementation, I got a lot of things right with Zod 3. The codebase has been versatile enough to supporting 23(!) minor releases, each with new features and enhancements, with no breaking changes to the public API.
17+
18+
But there are a couple recurring DX papercuts that will require structural changes to address, and that will involve breaking changes. (It's worth noting upfront that most Zod users will not be affected, but a lot of the libraries in Zod's ecosystem rely on internal APIs and will need to be updated.)
19+
20+
- To simplify the codebase and enable easier code generation tooling, some subclasses of `ZodType` will be split or consolidated.
21+
- To improve performance, the signature of the (quasi-)internal `_parse` method will be changed. Any user-defined subclasses of `ZodType` will need to be updated accordingly.
22+
- To clean up autocompletion, some internal methods and properties will be made `protected`. Some current APIs will be deprecated; some deprecated APIs will be removed.
23+
- To improve error reporting, I'll be simplifying Zod's error map system. The new system will also be more amenable to internationalization (RFC forthcoming).
24+
- To enable `exactOptionalPropertyTypes` semantics, the logic used to determine key optionality in `ZodObject` will change. Depending on the value of `exactOptionalPropertyTypes` in your `tsconfig.json`, some inferred types may change (RFC forthcoming).
25+
- To improve TypeScript server performance, some generic class signatures (e.g. `ZodUnion`) will be changed or simplified. Other type utilities will be re-implemented for efficiency, but may result in marginally different behavior in some contexts.
26+
27+
All told, Zod 4 will be a ground-up rewrite of the library with few breaking changes for typical users, dramatic speed improvements, a simpler internal structure, and a big slate of new features.
28+
29+
## Zod's current funding story
30+
31+
Zod's has [many generous donors](https://github.com/sponsors/colinhacks) and is likely one of the most well-sponsored TypeScript utility libraries of its kind. Right now, that works out to just over $2600/mo. I'm grateful for this level of support, and it far exceeds the expectations I had when I first set up my GitHub Sponsors profile.
32+
33+
But with much love and appreciation to all the people and companies that support Zod, that's far from replacing a full-time salary in the US.
34+
35+
I left Bun early this year and spent a couple months traveling, learning new things, and recovering from burnout. Starting in April, I spent about 6 weeks merging PRs and fixing issues, culminating in the release of Zod 3.23 (the final 3.x version). I've spent the last month or so spec'ing out Zod 4.
36+
37+
In my estimation it will take about three more months of full-time work to complete the rewrite and roll out the new release responsibly to Zod's now-massive base of users and third-party ecosystem libraries. I'm beyond excited to do all this work, but that's a long time to be without an income.
38+
39+
So I reached out to a few companies with an experimental proposal: an "OSS incubator" where the company would sponsor the development of Zod for 12 weeks (my timeline for the release of Zod 4). During this pre-determined window, I'd get paid some reasonable wage, and the company would be Zod's primary patron. The cost to the company is known upfront, since everything is term-limited; it's like an incubator or an internship.
40+
41+
## The Clerk fellowship
42+
43+
Much to my delight, [Colin](https://twitter.com/tweetsbycolin) from Clerk (AKA "other Colin") was enthusiastically on board. I've admired Clerk for a long time for their product, eye for developer experience, and commitment to open source. In fact, I covered them on my podcast the day they launched on HN in February 2021. They've already been sponsoring [Auth.js](https://authjs.dev) (formerly NextAuth) for some time and were immediately open to discussing the terms of a potential "fellowship".
44+
45+
In exchange for the support, Clerk is getting a super-charged version of the perks that Zod's other sponsors already get:
46+
47+
1. Diamond-tier placement in the README and the docs 💎 Big logo. Big. Huge.
48+
2. Updating my Twitter bio for the duration of the fellowship to reflect my new position as a Clerk OSS Fellow 💅
49+
3. Mentions in the forthcoming Zod 4 RFCs (Requests for Comment). Historically Zod's RFCs have attracted a lot of attention and feedback from the TypeScript community (or at least TypeScript Twitter). This is a perfect place to shout out the company that is (effectively) paying me to implement these new features.
50+
4. A small ad at the bottom of the sidebar of Zod's new docs site (under construction now). You can see what this might look like in the [Auth.js](https://authjs.dev/getting-started) docs.
51+
5. For continuity after the release of Zod 4, Clerk gets "first dibs" (right of first refusal) on a new ongoing "diamond tier" sponsor slot for 6 months. The idea is that this is an exclusive "slot"—only one company can hold it at a time.The perks of this tier include the big README logo and the sidebar ad placement.
52+
6. This announcement post! Yes, you've been reading marketing material this whole time. Gotcha.
53+
54+
## OSS, funding models, and trying new things
55+
56+
This model represents an interesting middle ground between the traditional sponsorship model and the "maintainer-in-residence" approach that companies like Vercel have taken with Rich Harris/Svelte. Zod doesn't need a full-time maintainer in perpetuity (actually, I wouldn't mind that... 🙄) but it does need full-time attention to get this major version out the door.
57+
58+
This fellowship is a way to bridge that gap. All-in-all, I'm beyond excited to have found a partner in Clerk that is interested in trying something like this.
59+
60+
> I encourage other companies to try similar things! There is no shortage of invaluable libraries with full-time (or nearly full-time) maintainers who are forgoing a regular income to build free tools. ArkType, Valibot, and tRPC come to mind.
61+
62+
So if you're building an app sometime soon, be smart—validate your `Request` bodies (or, uh, Server Action arguments?) and don't roll your own auth.
63+
64+

packages/docs/content/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"---Zod 4---",
55
"v4/index",
66
"v4/changelog",
7-
"v4/versioning",
7+
88
"---Documentation---",
99
"index",
1010
"basics",

packages/docs/loaders/source.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { docs } from "@/.source";
1+
import { blogPosts, docs } from "@/.source";
22
import { loader } from "fumadocs-core/source";
3+
import { createMDXSource } from "fumadocs-mdx";
34
import { icons } from "lucide-react";
45
import { createElement } from "react";
56

@@ -17,3 +18,9 @@ export const source = loader({
1718
if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
1819
},
1920
});
21+
22+
// Blog content loader
23+
export const blog = loader({
24+
baseUrl: "/blog",
25+
source: createMDXSource(blogPosts),
26+
});

packages/docs/source.config.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
1+
import { defineCollections, defineConfig, defineDocs } from "fumadocs-mdx/config";
22

33
export const docs = defineDocs({
44
dir: "content",
55
});
66

7+
// Blog collection (for content under `content/blog`)
8+
export const blogPosts = defineCollections({
9+
type: "doc",
10+
dir: "content/blog",
11+
// No schema to avoid zod version mismatch; frontmatter is optional
12+
});
13+
714
export default defineConfig({
815
mdxOptions: {
916
// MDX options

0 commit comments

Comments
 (0)