kaddidlehopper 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: content-collections
|
|
3
|
+
description: Use content-collections for type-safe content management with markdown files. Use when building blogs, documentation sites, or any content-driven pages with frontmatter and markdown.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: sdorra
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Content Collections
|
|
11
|
+
|
|
12
|
+
Content Collections transforms markdown and other content files into type-safe data collections with full TypeScript support.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Blog posts with frontmatter
|
|
17
|
+
- Documentation pages
|
|
18
|
+
- Content-driven sites
|
|
19
|
+
- Any structured content in markdown/JSON/YAML
|
|
20
|
+
- When you need type-safe content access
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @content-collections/core
|
|
26
|
+
npm install -D @content-collections/vite # For Vite/TanStack Start
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Basic Setup
|
|
30
|
+
|
|
31
|
+
### Configuration File
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// content-collections.ts
|
|
35
|
+
import { defineCollection, defineConfig } from '@content-collections/core';
|
|
36
|
+
import { z } from 'zod';
|
|
37
|
+
|
|
38
|
+
const posts = defineCollection({
|
|
39
|
+
name: 'posts',
|
|
40
|
+
directory: 'content/posts',
|
|
41
|
+
include: '**/*.md',
|
|
42
|
+
schema: (z) => ({
|
|
43
|
+
title: z.string(),
|
|
44
|
+
description: z.string().optional(),
|
|
45
|
+
published: z.string().date(),
|
|
46
|
+
author: z.string(),
|
|
47
|
+
tags: z.array(z.string()).optional(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
collections: [posts],
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Vite/TanStack Start Integration
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// app.config.ts
|
|
60
|
+
import { defineConfig } from '@tanstack/react-start/config';
|
|
61
|
+
import contentCollections from '@content-collections/vite';
|
|
62
|
+
|
|
63
|
+
export default defineConfig({
|
|
64
|
+
vite: {
|
|
65
|
+
plugins: [contentCollections()],
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Content File Structure
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
content/
|
|
74
|
+
├── posts/
|
|
75
|
+
│ ├── hello-world.md
|
|
76
|
+
│ ├── getting-started.md
|
|
77
|
+
│ └── advanced-topics.md
|
|
78
|
+
└── docs/
|
|
79
|
+
├── introduction.md
|
|
80
|
+
└── api-reference.md
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Markdown File Format
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
---
|
|
87
|
+
title: Hello World
|
|
88
|
+
description: My first blog post
|
|
89
|
+
published: 2024-01-15
|
|
90
|
+
author: Alice
|
|
91
|
+
tags:
|
|
92
|
+
- introduction
|
|
93
|
+
- tutorial
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
# Hello World
|
|
97
|
+
|
|
98
|
+
This is the content of my blog post.
|
|
99
|
+
|
|
100
|
+
## Getting Started
|
|
101
|
+
|
|
102
|
+
Here's how to get started...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Using Collections
|
|
106
|
+
|
|
107
|
+
### Import Generated Data
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
// Collections are auto-generated
|
|
111
|
+
import { allPosts } from 'content-collections';
|
|
112
|
+
|
|
113
|
+
// allPosts is an array of typed post objects
|
|
114
|
+
allPosts.forEach((post) => {
|
|
115
|
+
console.log(post.title); // string
|
|
116
|
+
console.log(post.published); // string (date)
|
|
117
|
+
console.log(post.tags); // string[] | undefined
|
|
118
|
+
console.log(post._meta.path); // file path without extension
|
|
119
|
+
console.log(post.content); // raw markdown content
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### In TanStack Start Routes
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// src/routes/blog.tsx
|
|
127
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
128
|
+
import { allPosts } from 'content-collections';
|
|
129
|
+
|
|
130
|
+
export const Route = createFileRoute('/blog')({
|
|
131
|
+
loader: () => {
|
|
132
|
+
// Sort by date, newest first
|
|
133
|
+
const posts = allPosts
|
|
134
|
+
.sort((a, b) =>
|
|
135
|
+
new Date(b.published).getTime() - new Date(a.published).getTime()
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return { posts };
|
|
139
|
+
},
|
|
140
|
+
component: BlogComponent,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
function BlogComponent() {
|
|
144
|
+
const { posts } = Route.useLoaderData();
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div>
|
|
148
|
+
<h1>Blog</h1>
|
|
149
|
+
<ul>
|
|
150
|
+
{posts.map((post) => (
|
|
151
|
+
<li key={post._meta.path}>
|
|
152
|
+
<Link to="/blog/$slug" params={{ slug: post._meta.path }}>
|
|
153
|
+
<h2>{post.title}</h2>
|
|
154
|
+
<p>{post.description}</p>
|
|
155
|
+
<time>{post.published}</time>
|
|
156
|
+
</Link>
|
|
157
|
+
</li>
|
|
158
|
+
))}
|
|
159
|
+
</ul>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Individual Post Page
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
// src/routes/blog.$slug.tsx
|
|
169
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
170
|
+
import { allPosts } from 'content-collections';
|
|
171
|
+
|
|
172
|
+
export const Route = createFileRoute('/blog/$slug')({
|
|
173
|
+
loader: ({ params }) => {
|
|
174
|
+
const post = allPosts.find((p) => p._meta.path === params.slug);
|
|
175
|
+
|
|
176
|
+
if (!post) {
|
|
177
|
+
throw new Error('Post not found');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { post };
|
|
181
|
+
},
|
|
182
|
+
component: PostComponent,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
function PostComponent() {
|
|
186
|
+
const { post } = Route.useLoaderData();
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<article>
|
|
190
|
+
<h1>{post.title}</h1>
|
|
191
|
+
<p>By {post.author} on {post.published}</p>
|
|
192
|
+
|
|
193
|
+
{/* Render markdown content */}
|
|
194
|
+
<div dangerouslySetInnerHTML={{ __html: post.html }} />
|
|
195
|
+
</article>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Transforming Content
|
|
201
|
+
|
|
202
|
+
### Compile Markdown to HTML
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// content-collections.ts
|
|
206
|
+
import { defineCollection, defineConfig } from '@content-collections/core';
|
|
207
|
+
import { compileMarkdown } from '@content-collections/markdown';
|
|
208
|
+
|
|
209
|
+
const posts = defineCollection({
|
|
210
|
+
name: 'posts',
|
|
211
|
+
directory: 'content/posts',
|
|
212
|
+
include: '**/*.md',
|
|
213
|
+
schema: (z) => ({
|
|
214
|
+
title: z.string(),
|
|
215
|
+
published: z.string().date(),
|
|
216
|
+
}),
|
|
217
|
+
transform: async (document, context) => {
|
|
218
|
+
// Compile markdown to HTML
|
|
219
|
+
const html = await compileMarkdown(context, document);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
...document,
|
|
223
|
+
html,
|
|
224
|
+
// Add computed fields
|
|
225
|
+
slug: document._meta.path,
|
|
226
|
+
readingTime: calculateReadingTime(document.content),
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
function calculateReadingTime(content: string): number {
|
|
232
|
+
const wordsPerMinute = 200;
|
|
233
|
+
const words = content.split(/\s+/).length;
|
|
234
|
+
return Math.ceil(words / wordsPerMinute);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Install Markdown Package
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npm install @content-collections/markdown
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Advanced Markdown with Plugins
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { compileMarkdown } from '@content-collections/markdown';
|
|
248
|
+
import remarkGfm from 'remark-gfm';
|
|
249
|
+
import rehypeHighlight from 'rehype-highlight';
|
|
250
|
+
|
|
251
|
+
const posts = defineCollection({
|
|
252
|
+
name: 'posts',
|
|
253
|
+
directory: 'content/posts',
|
|
254
|
+
include: '**/*.md',
|
|
255
|
+
schema: (z) => ({
|
|
256
|
+
title: z.string(),
|
|
257
|
+
}),
|
|
258
|
+
transform: async (document, context) => {
|
|
259
|
+
const html = await compileMarkdown(context, document, {
|
|
260
|
+
remarkPlugins: [remarkGfm],
|
|
261
|
+
rehypePlugins: [rehypeHighlight],
|
|
262
|
+
allowDangerousHtml: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return { ...document, html };
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Multiple Collections
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
// content-collections.ts
|
|
274
|
+
import { defineCollection, defineConfig } from '@content-collections/core';
|
|
275
|
+
|
|
276
|
+
const posts = defineCollection({
|
|
277
|
+
name: 'posts',
|
|
278
|
+
directory: 'content/posts',
|
|
279
|
+
include: '**/*.md',
|
|
280
|
+
schema: (z) => ({
|
|
281
|
+
title: z.string(),
|
|
282
|
+
published: z.string().date(),
|
|
283
|
+
author: z.string(),
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const docs = defineCollection({
|
|
288
|
+
name: 'docs',
|
|
289
|
+
directory: 'content/docs',
|
|
290
|
+
include: '**/*.md',
|
|
291
|
+
schema: (z) => ({
|
|
292
|
+
title: z.string(),
|
|
293
|
+
order: z.number().optional(),
|
|
294
|
+
category: z.string().optional(),
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const authors = defineCollection({
|
|
299
|
+
name: 'authors',
|
|
300
|
+
directory: 'content/authors',
|
|
301
|
+
include: '**/*.json',
|
|
302
|
+
schema: (z) => ({
|
|
303
|
+
name: z.string(),
|
|
304
|
+
email: z.string().email(),
|
|
305
|
+
bio: z.string().optional(),
|
|
306
|
+
avatar: z.string().optional(),
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
export default defineConfig({
|
|
311
|
+
collections: [posts, docs, authors],
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Usage
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
import { allPosts, allDocs, allAuthors } from 'content-collections';
|
|
319
|
+
|
|
320
|
+
// Each collection is independently typed
|
|
321
|
+
const post = allPosts[0];
|
|
322
|
+
const doc = allDocs[0];
|
|
323
|
+
const author = allAuthors[0];
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Joining Collections
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// content-collections.ts
|
|
330
|
+
const posts = defineCollection({
|
|
331
|
+
name: 'posts',
|
|
332
|
+
directory: 'content/posts',
|
|
333
|
+
include: '**/*.md',
|
|
334
|
+
schema: (z) => ({
|
|
335
|
+
title: z.string(),
|
|
336
|
+
authorId: z.string(), // Reference to author
|
|
337
|
+
}),
|
|
338
|
+
transform: async (document, context) => {
|
|
339
|
+
// Find the author from the authors collection
|
|
340
|
+
const author = context
|
|
341
|
+
.documents(authors)
|
|
342
|
+
.find((a) => a._meta.path === document.authorId);
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
...document,
|
|
346
|
+
author: author ? {
|
|
347
|
+
name: author.name,
|
|
348
|
+
avatar: author.avatar,
|
|
349
|
+
} : null,
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## _meta Object
|
|
356
|
+
|
|
357
|
+
Every document includes a `_meta` object:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
{
|
|
361
|
+
_meta: {
|
|
362
|
+
path: "hello-world", // File path without extension
|
|
363
|
+
fileName: "hello-world.md",
|
|
364
|
+
directory: "content/posts",
|
|
365
|
+
extension: "md",
|
|
366
|
+
filePath: "content/posts/hello-world.md",
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Schema Validation
|
|
372
|
+
|
|
373
|
+
Content Collections uses Zod for schema validation:
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
const posts = defineCollection({
|
|
377
|
+
name: 'posts',
|
|
378
|
+
directory: 'content/posts',
|
|
379
|
+
include: '**/*.md',
|
|
380
|
+
schema: (z) => ({
|
|
381
|
+
// Required fields
|
|
382
|
+
title: z.string().min(1).max(100),
|
|
383
|
+
published: z.string().date(),
|
|
384
|
+
|
|
385
|
+
// Optional fields
|
|
386
|
+
description: z.string().optional(),
|
|
387
|
+
draft: z.boolean().default(false),
|
|
388
|
+
|
|
389
|
+
// Arrays
|
|
390
|
+
tags: z.array(z.string()).default([]),
|
|
391
|
+
|
|
392
|
+
// Enums
|
|
393
|
+
category: z.enum(['tech', 'life', 'tutorial']),
|
|
394
|
+
|
|
395
|
+
// Complex types
|
|
396
|
+
author: z.object({
|
|
397
|
+
name: z.string(),
|
|
398
|
+
email: z.string().email(),
|
|
399
|
+
}),
|
|
400
|
+
|
|
401
|
+
// Coercion
|
|
402
|
+
views: z.coerce.number().default(0),
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Development Workflow
|
|
408
|
+
|
|
409
|
+
### Hot Module Replacement
|
|
410
|
+
|
|
411
|
+
Content Collections supports HMR - changes to content files automatically update:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
npm run dev
|
|
415
|
+
# Edit content/posts/hello-world.md
|
|
416
|
+
# Changes appear instantly in browser
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Build Validation
|
|
420
|
+
|
|
421
|
+
Invalid content fails the build:
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
npm run build
|
|
425
|
+
# Error: posts/bad-post.md - "published" is required
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Directory Structure Best Practice
|
|
429
|
+
|
|
430
|
+
```
|
|
431
|
+
project/
|
|
432
|
+
├── content/
|
|
433
|
+
│ ├── posts/
|
|
434
|
+
│ │ ├── 2024/
|
|
435
|
+
│ │ │ ├── hello-world.md
|
|
436
|
+
│ │ │ └── another-post.md
|
|
437
|
+
│ │ └── 2023/
|
|
438
|
+
│ │ └── old-post.md
|
|
439
|
+
│ ├── docs/
|
|
440
|
+
│ │ ├── getting-started.md
|
|
441
|
+
│ │ └── api/
|
|
442
|
+
│ │ └── reference.md
|
|
443
|
+
│ └── authors/
|
|
444
|
+
│ ├── alice.json
|
|
445
|
+
│ └── bob.json
|
|
446
|
+
├── content-collections.ts
|
|
447
|
+
├── app.config.ts
|
|
448
|
+
└── src/
|
|
449
|
+
└── routes/
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## TypeScript Support
|
|
453
|
+
|
|
454
|
+
Full type inference for all collections:
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
import { allPosts } from 'content-collections';
|
|
458
|
+
import type { Post } from 'content-collections';
|
|
459
|
+
|
|
460
|
+
// Type is inferred
|
|
461
|
+
const post = allPosts[0];
|
|
462
|
+
post.title; // string
|
|
463
|
+
post.published; // string
|
|
464
|
+
post.tags; // string[] | undefined
|
|
465
|
+
|
|
466
|
+
// Or use the generated type
|
|
467
|
+
function renderPost(post: Post) {
|
|
468
|
+
return <h1>{post.title}</h1>;
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Common Patterns
|
|
473
|
+
|
|
474
|
+
### Filter Published Posts
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
const publishedPosts = allPosts.filter((post) => !post.draft);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Sort by Date
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
const sortedPosts = allPosts.sort(
|
|
484
|
+
(a, b) => new Date(b.published).getTime() - new Date(a.published).getTime()
|
|
485
|
+
);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Group by Category
|
|
489
|
+
|
|
490
|
+
```typescript
|
|
491
|
+
const postsByCategory = allPosts.reduce((acc, post) => {
|
|
492
|
+
const category = post.category || 'uncategorized';
|
|
493
|
+
acc[category] = acc[category] || [];
|
|
494
|
+
acc[category].push(post);
|
|
495
|
+
return acc;
|
|
496
|
+
}, {} as Record<string, typeof allPosts>);
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### Get Post by Slug
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
function getPostBySlug(slug: string) {
|
|
503
|
+
return allPosts.find((post) => post._meta.path === slug);
|
|
504
|
+
}
|
|
505
|
+
```
|