Skip to content

Commit 87d4b6c

Browse files
committed
refactor: migrate content management from contentlayer to content-collections
1 parent a78d130 commit 87d4b6c

File tree

17 files changed

+7030
-8317
lines changed

17 files changed

+7030
-8317
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ next-env.d.ts
4040
/.react-email/
4141

4242
.vscode
43-
.contentlayer
43+
.content-collections

app/(docs)/docs/[[...slug]]/page.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from "next/navigation";
2-
import { allDocs } from "contentlayer/generated";
2+
import { allDocs } from "content-collections";
33

44
import { getTableOfContents } from "@/lib/toc";
55
import { Mdx } from "@/components/content/mdx-components";
@@ -20,7 +20,8 @@ interface DocPageProps {
2020
}
2121

2222
async function getDocFromParams(params) {
23-
const slug = params.slug?.join("/") || "";
23+
const slug = params.slug?.join("/") || "index";
24+
2425
const doc = allDocs.find((doc) => doc.slugAsParams === slug);
2526

2627
if (!doc) return null;
@@ -58,11 +59,12 @@ export default async function DocPage({ params }: DocPageProps) {
5859
notFound();
5960
}
6061

61-
const toc = await getTableOfContents(doc.body.raw);
62+
const toc = await getTableOfContents(doc.content);
6263

6364
const images = await Promise.all(
6465
doc.images.map(async (src: string) => ({
6566
src,
67+
alt: "Image",
6668
blurDataURL: await getBlurDataURL(src),
6769
})),
6870
);
@@ -72,7 +74,7 @@ export default async function DocPage({ params }: DocPageProps) {
7274
<div className="mx-auto w-full min-w-0">
7375
<DocsPageHeader heading={doc.title} text={doc.description} />
7476
<div className="pb-4 pt-11">
75-
<Mdx code={doc.body.code} images={images} />
77+
<Mdx code={doc.body} images={images} />
7678
</div>
7779
<hr className="my-4 md:my-6" />
7880
<DocsPager doc={doc} />

app/(docs)/guides/[slug]/page.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { allGuides } from "contentlayer/generated";
1+
import { allGuides } from "content-collections";
22
import Link from "next/link";
33
import { notFound } from "next/navigation";
44

@@ -53,14 +53,14 @@ export default async function GuidePage({
5353
notFound();
5454
}
5555

56-
const toc = await getTableOfContents(guide.body.raw);
56+
const toc = await getTableOfContents(guide.body);
5757

5858
return (
5959
<MaxWidthWrapper>
6060
<div className="relative py-6 lg:grid lg:grid-cols-[1fr_300px] lg:gap-10 lg:py-10 xl:gap-20">
6161
<div>
6262
<DocsPageHeader heading={guide.title} text={guide.description} />
63-
<Mdx code={guide.body.code} />
63+
<Mdx code={guide.body} />
6464
<hr className="my-4" />
6565
<div className="flex justify-center py-6 lg:py-10">
6666
<Link

app/(docs)/guides/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link";
2-
import { allGuides } from "contentlayer/generated";
2+
import { allGuides } from "content-collections";
33
import { compareDesc } from "date-fns";
44

55
import { formatDate } from "@/lib/utils";

app/(marketing)/(blog-post)/blog/[slug]/page.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from "next/navigation";
2-
import { allPosts } from "contentlayer/generated";
2+
import { allPosts } from "content-collections";
33

44
import { Mdx } from "@/components/content/mdx-components";
55

@@ -72,13 +72,14 @@ export default async function PostPage({
7272
)) ||
7373
[];
7474

75-
const toc = await getTableOfContents(post.body.raw);
75+
const toc = await getTableOfContents(post.content);
7676

7777
const [thumbnailBlurhash, images] = await Promise.all([
7878
getBlurDataURL(post.image),
7979
await Promise.all(
8080
post.images.map(async (src: string) => ({
8181
src,
82+
alt: "image",
8283
blurDataURL: await getBlurDataURL(src),
8384
})),
8485
),
@@ -140,7 +141,7 @@ export default async function PostPage({
140141
sizes="(max-width: 768px) 770px, 1000px"
141142
/>
142143
<div className="px-[.8rem] pb-10 md:px-8">
143-
<Mdx code={post.body.code} images={images} />
144+
<Mdx code={post.body} images={images} />
144145
</div>
145146
</div>
146147

app/(marketing)/[slug]/page.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { notFound } from "next/navigation";
2-
import { allPages } from "contentlayer/generated";
2+
import { allPages } from "content-collections";
33

44
import { Mdx } from "@/components/content/mdx-components";
55

@@ -49,6 +49,7 @@ export default async function PagePage({
4949
const images = await Promise.all(
5050
page.images.map(async (src: string) => ({
5151
src,
52+
alt: "image",
5253
blurDataURL: await getBlurDataURL(src),
5354
})),
5455
);
@@ -64,7 +65,7 @@ export default async function PagePage({
6465
)}
6566
</div>
6667
<hr className="my-4" />
67-
<Mdx code={page.body.code} images={images} />
68+
<Mdx code={page.body} images={images} />
6869
</article>
6970
);
7071
}

app/(marketing)/blog/category/[slug]/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Metadata } from "next";
22
import { notFound } from "next/navigation";
3-
import { allPosts } from "contentlayer/generated";
3+
import { allPosts } from "content-collections";
44

55
import { BLOG_CATEGORIES } from "@/config/blog";
66
import { constructMetadata, getBlurDataURL } from "@/lib/utils";

app/(marketing)/blog/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { allPosts } from "contentlayer/generated";
1+
import { allPosts } from "content-collections";
22

33
import { constructMetadata, getBlurDataURL } from "@/lib/utils";
44
import { BlogPosts } from "@/components/content/blog-posts";

components/content/blog-card.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link";
2-
import { Post } from "contentlayer/generated";
2+
import { Post } from "content-collections";
33

44
import { cn, formatDate, placeholderBlurhash } from "@/lib/utils";
55
import BlurImage from "@/components/shared/blur-image";

components/content/blog-posts.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Post } from "@/.contentlayer/generated";
1+
import { Post } from "content-collections";
22

33
import { BlogCard } from "./blog-card";
44

components/content/mdx-components.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from "react";
22
import NextImage, { ImageProps } from "next/image";
33
import Link from "next/link";
4-
import { useMDXComponent } from "next-contentlayer2/hooks";
4+
import { useMDXComponent } from "@content-collections/mdx/react";
55

66
import { cn } from "@/lib/utils";
77
import { MdxCard } from "@/components/content/mdx-card";

components/docs/pager.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Link from "next/link"
2-
import { Doc } from "contentlayer/generated"
2+
import { Doc } from "content-collections"
33

44
import { docsConfig } from "@/config/docs"
55
import { cn } from "@/lib/utils"

content-collections.ts

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import {
2+
defineCollection,
3+
defineConfig,
4+
Context,
5+
Document,
6+
} from "@content-collections/core";
7+
import rehypeAutolinkHeadings from "rehype-autolink-headings";
8+
import rehypePrettyCode from "rehype-pretty-code";
9+
import rehypeSlug from "rehype-slug";
10+
import remarkGfm from "remark-gfm";
11+
import { visit } from "unist-util-visit";
12+
import { compileMDX } from "@content-collections/mdx";
13+
14+
type Transformed = {
15+
_id: string
16+
body: string;
17+
slug: string;
18+
images: string[];
19+
slugAsParams: string;
20+
}
21+
22+
const transform = async <T extends Document & { content: string }>(
23+
doc: T,
24+
context: Context
25+
): Promise<T & Transformed> => {
26+
const body = await compileMDX(context, doc, {
27+
remarkPlugins: [remarkGfm],
28+
rehypePlugins: [
29+
rehypeSlug,
30+
() => (tree) => {
31+
visit(tree, (node) => {
32+
if (node?.type === "element" && node?.tagName === "pre") {
33+
const [codeEl] = node.children;
34+
if (codeEl.tagName !== "code") return;
35+
node.rawString = codeEl.children?.[0].value;
36+
}
37+
});
38+
},
39+
[
40+
rehypePrettyCode,
41+
{
42+
theme: "github-dark",
43+
keepBackground: false,
44+
onVisitLine(node) {
45+
// Prevent lines from collapsing in `display: grid` mode, and allow empty lines to be copy/pasted
46+
if (node.children.length === 0) {
47+
node.children = [{ type: "text", value: " " }];
48+
}
49+
},
50+
},
51+
],
52+
() => (tree) => {
53+
visit(tree, (node) => {
54+
if (node?.type === "element" && node?.tagName === "figure") {
55+
if (!("data-rehype-pretty-code-figure" in node.properties)) {
56+
return;
57+
}
58+
const preElement = node.children.at(-1);
59+
if (preElement.tagName !== "pre") {
60+
return;
61+
}
62+
preElement.properties["rawString"] = node.rawString;
63+
}
64+
});
65+
},
66+
[
67+
rehypeAutolinkHeadings,
68+
{
69+
properties: {
70+
className: ["subheading-anchor"],
71+
ariaLabel: "Link to section",
72+
},
73+
},
74+
],
75+
],
76+
});
77+
78+
const imageMatches =
79+
doc.content.match(/(?<=<Image[^>]\bsrc=")[^"]+(?="[^>]\/>)/g) || [];
80+
81+
const rootPath = context.collection.directory.split("/").slice(1).join("/")
82+
83+
const transformedDoc: T & Transformed = {
84+
...doc,
85+
body,
86+
images: imageMatches,
87+
_id: doc._meta.filePath,
88+
slugAsParams: doc._meta.path,
89+
slug: `/${rootPath}/${doc._meta.path}`
90+
};
91+
92+
return transformedDoc;
93+
};
94+
95+
const Page = defineCollection({
96+
name: "Page",
97+
directory: "content/pages",
98+
include: "**/*.mdx",
99+
schema: (z) => ({
100+
title: z.string(),
101+
description: z.string().optional(),
102+
}),
103+
transform,
104+
});
105+
106+
const Doc = defineCollection({
107+
name: "Doc",
108+
directory: "content/docs",
109+
include: "**/*.mdx",
110+
schema: (z) => ({
111+
title: z.string(),
112+
description: z.string().optional(),
113+
published: z.boolean().default(true),
114+
}),
115+
transform,
116+
});
117+
118+
const Guide = defineCollection({
119+
name: "Guide",
120+
directory: "content/guides",
121+
include: "**/*.mdx",
122+
schema: (z) => ({
123+
title: z.string(),
124+
description: z.string().optional(),
125+
date: z.string(),
126+
published: z.boolean().default(true),
127+
featured: z.boolean().default(false),
128+
}),
129+
transform,
130+
});
131+
132+
const Post = defineCollection({
133+
name: "Post",
134+
directory: "content/blog",
135+
include: "**/*.mdx",
136+
schema: (z) => ({
137+
title: z.string(),
138+
description: z.string().optional(),
139+
date: z.string(),
140+
published: z.boolean().default(true),
141+
image: z.string(),
142+
authors: z.array(z.string()),
143+
categories: z.array(z.enum(["news", "education"])).default(["news"]),
144+
related: z.array(z.string()).optional(),
145+
}),
146+
transform,
147+
});
148+
149+
export default defineConfig({
150+
collections: [Page, Doc, Guide, Post],
151+
});

next.config.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { withContentlayer } = require("next-contentlayer2");
1+
const { withContentCollections } = require("@content-collections/next");
22

33
import("./env.mjs");
44

@@ -27,4 +27,4 @@ const nextConfig = {
2727
},
2828
};
2929

30-
module.exports = withContentlayer(nextConfig);
30+
module.exports = withContentCollections(nextConfig);

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,11 @@
5959
"clsx": "^2.1.1",
6060
"cmdk": "^1.0.0",
6161
"concurrently": "^8.2.2",
62-
"contentlayer2": "^0.5.0",
6362
"date-fns": "^3.6.0",
6463
"lucide-react": "^0.414.0",
6564
"ms": "^2.1.3",
6665
"next": "14.2.5",
6766
"next-auth": "5.0.0-beta.19",
68-
"next-contentlayer2": "^0.5.0",
6967
"next-themes": "^0.3.0",
7068
"prop-types": "^15.8.1",
7169
"react": "18.3.1",
@@ -88,6 +86,9 @@
8886
"devDependencies": {
8987
"@commitlint/cli": "^19.3.0",
9088
"@commitlint/config-conventional": "^19.2.2",
89+
"@content-collections/core": "^0.7.2",
90+
"@content-collections/mdx": "^0.2.0",
91+
"@content-collections/next": "^0.2.3",
9192
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
9293
"@tailwindcss/line-clamp": "^0.4.4",
9394
"@tailwindcss/typography": "^0.5.13",

0 commit comments

Comments
 (0)