Initial commit
This commit is contained in:
779
skills/astro-db-patterns/SKILL.md
Normal file
779
skills/astro-db-patterns/SKILL.md
Normal file
@@ -0,0 +1,779 @@
|
||||
# Astro DB Patterns Skill
|
||||
|
||||
Common patterns and examples for using Astro DB, including schema design, queries, and data seeding.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up Astro DB in a project
|
||||
- Designing database schemas
|
||||
- Writing queries with Drizzle ORM
|
||||
- Seeding development data
|
||||
- Building API endpoints with database access
|
||||
|
||||
## Database Setup
|
||||
|
||||
### Install Astro DB
|
||||
|
||||
```bash
|
||||
npx astro add db
|
||||
```
|
||||
|
||||
### Configuration File
|
||||
|
||||
Create `db/config.ts`:
|
||||
|
||||
```typescript
|
||||
import { defineDb, defineTable, column } from 'astro:db';
|
||||
|
||||
const MyTable = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
// ... other columns
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { MyTable }
|
||||
});
|
||||
```
|
||||
|
||||
## Column Types
|
||||
|
||||
### Text Columns
|
||||
|
||||
```typescript
|
||||
// Basic text
|
||||
name: column.text()
|
||||
|
||||
// Unique text
|
||||
email: column.text({ unique: true })
|
||||
|
||||
// Optional text
|
||||
bio: column.text({ optional: true })
|
||||
|
||||
// Text with default
|
||||
status: column.text({ default: 'active' })
|
||||
```
|
||||
|
||||
### Number Columns
|
||||
|
||||
```typescript
|
||||
// Basic number
|
||||
age: column.number()
|
||||
|
||||
// Primary key
|
||||
id: column.number({ primaryKey: true })
|
||||
|
||||
// Optional number
|
||||
rating: column.number({ optional: true })
|
||||
|
||||
// Number with default
|
||||
views: column.number({ default: 0 })
|
||||
|
||||
// Unique number
|
||||
userId: column.number({ unique: true })
|
||||
```
|
||||
|
||||
### Boolean Columns
|
||||
|
||||
```typescript
|
||||
// Basic boolean
|
||||
published: column.boolean()
|
||||
|
||||
// Boolean with default
|
||||
active: column.boolean({ default: true })
|
||||
featured: column.boolean({ default: false })
|
||||
|
||||
// Optional boolean
|
||||
verified: column.boolean({ optional: true })
|
||||
```
|
||||
|
||||
### Date Columns
|
||||
|
||||
```typescript
|
||||
// Basic date
|
||||
createdAt: column.date()
|
||||
|
||||
// Optional date
|
||||
publishedAt: column.date({ optional: true })
|
||||
|
||||
// Date with default (requires runtime value)
|
||||
updatedAt: column.date({ default: new Date() })
|
||||
```
|
||||
|
||||
### JSON Columns
|
||||
|
||||
```typescript
|
||||
// JSON data
|
||||
metadata: column.json()
|
||||
|
||||
// JSON with default
|
||||
settings: column.json({ default: {} })
|
||||
options: column.json({ default: [] })
|
||||
|
||||
// Optional JSON
|
||||
extra: column.json({ optional: true })
|
||||
```
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
### Blog Database
|
||||
|
||||
```typescript
|
||||
import { defineDb, defineTable, column } from 'astro:db';
|
||||
|
||||
const Author = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
email: column.text({ unique: true }),
|
||||
bio: column.text({ optional: true }),
|
||||
avatar: column.text({ optional: true }),
|
||||
website: column.text({ optional: true }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Post = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
title: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
content: column.text(),
|
||||
excerpt: column.text({ optional: true }),
|
||||
published: column.boolean({ default: false }),
|
||||
publishedAt: column.date({ optional: true }),
|
||||
updatedAt: column.date({ optional: true }),
|
||||
authorId: column.number(),
|
||||
views: column.number({ default: 0 }),
|
||||
featured: column.boolean({ default: false })
|
||||
}
|
||||
});
|
||||
|
||||
const Tag = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text({ unique: true }),
|
||||
slug: column.text({ unique: true })
|
||||
}
|
||||
});
|
||||
|
||||
const PostTag = defineTable({
|
||||
columns: {
|
||||
postId: column.number(),
|
||||
tagId: column.number()
|
||||
}
|
||||
});
|
||||
|
||||
const Comment = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
postId: column.number(),
|
||||
author: column.text(),
|
||||
email: column.text(),
|
||||
content: column.text(),
|
||||
approved: column.boolean({ default: false }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { Author, Post, Tag, PostTag, Comment }
|
||||
});
|
||||
```
|
||||
|
||||
### E-commerce Database
|
||||
|
||||
```typescript
|
||||
const Product = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
description: column.text(),
|
||||
price: column.number(),
|
||||
salePrice: column.number({ optional: true }),
|
||||
sku: column.text({ unique: true }),
|
||||
inStock: column.boolean({ default: true }),
|
||||
quantity: column.number({ default: 0 }),
|
||||
categoryId: column.number(),
|
||||
images: column.json({ default: [] }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Category = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
name: column.text(),
|
||||
slug: column.text({ unique: true }),
|
||||
description: column.text({ optional: true }),
|
||||
parentId: column.number({ optional: true })
|
||||
}
|
||||
});
|
||||
|
||||
const Order = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
orderNumber: column.text({ unique: true }),
|
||||
customerId: column.number(),
|
||||
total: column.number(),
|
||||
status: column.text({ default: 'pending' }),
|
||||
createdAt: column.date(),
|
||||
completedAt: column.date({ optional: true })
|
||||
}
|
||||
});
|
||||
|
||||
const OrderItem = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
orderId: column.number(),
|
||||
productId: column.number(),
|
||||
quantity: column.number(),
|
||||
price: column.number()
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { Product, Category, Order, OrderItem }
|
||||
});
|
||||
```
|
||||
|
||||
### User Management Database
|
||||
|
||||
```typescript
|
||||
const User = defineTable({
|
||||
columns: {
|
||||
id: column.number({ primaryKey: true }),
|
||||
username: column.text({ unique: true }),
|
||||
email: column.text({ unique: true }),
|
||||
passwordHash: column.text(),
|
||||
firstName: column.text(),
|
||||
lastName: column.text(),
|
||||
role: column.text({ default: 'user' }),
|
||||
active: column.boolean({ default: true }),
|
||||
emailVerified: column.boolean({ default: false }),
|
||||
lastLogin: column.date({ optional: true }),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const Session = defineTable({
|
||||
columns: {
|
||||
id: column.text({ primaryKey: true }),
|
||||
userId: column.number(),
|
||||
expiresAt: column.date(),
|
||||
createdAt: column.date()
|
||||
}
|
||||
});
|
||||
|
||||
const UserProfile = defineTable({
|
||||
columns: {
|
||||
userId: column.number({ unique: true }),
|
||||
bio: column.text({ optional: true }),
|
||||
avatar: column.text({ optional: true }),
|
||||
location: column.text({ optional: true }),
|
||||
website: column.text({ optional: true }),
|
||||
social: column.json({ default: {} })
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDb({
|
||||
tables: { User, Session, UserProfile }
|
||||
});
|
||||
```
|
||||
|
||||
## Data Seeding
|
||||
|
||||
### Basic Seed File
|
||||
|
||||
Create `db/seed.ts`:
|
||||
|
||||
```typescript
|
||||
import { db, Author, Post, Tag } from 'astro:db';
|
||||
|
||||
export default async function seed() {
|
||||
// Insert authors
|
||||
await db.insert(Author).values([
|
||||
{
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
bio: 'Tech enthusiast and blogger',
|
||||
createdAt: new Date('2024-01-01')
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Jane Smith',
|
||||
email: 'jane@example.com',
|
||||
bio: 'Software developer',
|
||||
createdAt: new Date('2024-01-01')
|
||||
}
|
||||
]);
|
||||
|
||||
// Insert tags
|
||||
await db.insert(Tag).values([
|
||||
{ id: 1, name: 'Astro', slug: 'astro' },
|
||||
{ id: 2, name: 'JavaScript', slug: 'javascript' },
|
||||
{ id: 3, name: 'Tutorial', slug: 'tutorial' }
|
||||
]);
|
||||
|
||||
// Insert posts
|
||||
await db.insert(Post).values([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Getting Started with Astro',
|
||||
slug: 'getting-started-astro',
|
||||
content: 'Full post content here...',
|
||||
excerpt: 'Learn the basics of Astro',
|
||||
published: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
authorId: 1,
|
||||
views: 150,
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Advanced Astro Patterns',
|
||||
slug: 'advanced-astro',
|
||||
content: 'Advanced content...',
|
||||
published: false,
|
||||
authorId: 2,
|
||||
views: 0
|
||||
}
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Seed with Relationships
|
||||
|
||||
```typescript
|
||||
import { db, Post, Tag, PostTag, Comment } from 'astro:db';
|
||||
|
||||
export default async function seed() {
|
||||
// ... insert posts and tags ...
|
||||
|
||||
// Link posts to tags (many-to-many)
|
||||
await db.insert(PostTag).values([
|
||||
{ postId: 1, tagId: 1 }, // Post 1 -> Astro
|
||||
{ postId: 1, tagId: 3 }, // Post 1 -> Tutorial
|
||||
{ postId: 2, tagId: 1 }, // Post 2 -> Astro
|
||||
{ postId: 2, tagId: 2 } // Post 2 -> JavaScript
|
||||
]);
|
||||
|
||||
// Add comments
|
||||
await db.insert(Comment).values([
|
||||
{
|
||||
id: 1,
|
||||
postId: 1,
|
||||
author: 'Reader',
|
||||
email: 'reader@example.com',
|
||||
content: 'Great post!',
|
||||
approved: true,
|
||||
createdAt: new Date()
|
||||
}
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### Select All
|
||||
|
||||
```typescript
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
const allPosts = await db.select().from(Post);
|
||||
```
|
||||
|
||||
### Select with Filter
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
// Published posts
|
||||
const publishedPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.published, true));
|
||||
|
||||
// Post by slug
|
||||
const post = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.slug, 'my-post'))
|
||||
.get(); // Returns single result or undefined
|
||||
```
|
||||
|
||||
### Multiple Conditions
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq, gt, and, or } from 'astro:db';
|
||||
|
||||
// AND condition
|
||||
const featuredPublished = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(and(
|
||||
eq(Post.published, true),
|
||||
eq(Post.featured, true)
|
||||
));
|
||||
|
||||
// OR condition
|
||||
const popularOrFeatured = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(or(
|
||||
gt(Post.views, 1000),
|
||||
eq(Post.featured, true)
|
||||
));
|
||||
```
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
```typescript
|
||||
import { db, Post, gt, gte, lt, lte, ne, like } from 'astro:db';
|
||||
|
||||
// Greater than
|
||||
const popularPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(gt(Post.views, 100));
|
||||
|
||||
// Greater than or equal
|
||||
const recentPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(gte(Post.publishedAt, new Date('2024-01-01')));
|
||||
|
||||
// Less than
|
||||
const drafts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(lt(Post.views, 10));
|
||||
|
||||
// Not equal
|
||||
const notDrafts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(ne(Post.published, false));
|
||||
|
||||
// LIKE pattern matching
|
||||
const astroPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(like(Post.title, '%Astro%'));
|
||||
```
|
||||
|
||||
### Ordering Results
|
||||
|
||||
```typescript
|
||||
import { db, Post, desc, asc } from 'astro:db';
|
||||
|
||||
// Descending order
|
||||
const latestPosts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt));
|
||||
|
||||
// Ascending order
|
||||
const oldestFirst = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(asc(Post.createdAt));
|
||||
|
||||
// Multiple order columns
|
||||
const sorted = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.featured), desc(Post.publishedAt));
|
||||
```
|
||||
|
||||
### Limit and Offset
|
||||
|
||||
```typescript
|
||||
import { db, Post, desc } from 'astro:db';
|
||||
|
||||
// Latest 10 posts
|
||||
const latest = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt))
|
||||
.limit(10);
|
||||
|
||||
// Pagination
|
||||
const page = 2;
|
||||
const perPage = 10;
|
||||
const paginated = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage);
|
||||
```
|
||||
|
||||
### Joins
|
||||
|
||||
```typescript
|
||||
import { db, Post, Author, eq } from 'astro:db';
|
||||
|
||||
// Inner join
|
||||
const postsWithAuthors = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.innerJoin(Author, eq(Post.authorId, Author.id));
|
||||
|
||||
// Access joined data
|
||||
postsWithAuthors.forEach(row => {
|
||||
console.log(row.Post.title);
|
||||
console.log(row.Author.name);
|
||||
});
|
||||
```
|
||||
|
||||
### Complex Join Query
|
||||
|
||||
```typescript
|
||||
import { db, Post, Author, Tag, PostTag, eq } from 'astro:db';
|
||||
|
||||
// Posts with authors and tags
|
||||
const postsWithDetails = await db
|
||||
.select({
|
||||
post: Post,
|
||||
author: Author,
|
||||
tag: Tag
|
||||
})
|
||||
.from(Post)
|
||||
.innerJoin(Author, eq(Post.authorId, Author.id))
|
||||
.innerJoin(PostTag, eq(Post.id, PostTag.postId))
|
||||
.innerJoin(Tag, eq(PostTag.tagId, Tag.id));
|
||||
```
|
||||
|
||||
## Insert, Update, Delete
|
||||
|
||||
### Insert Single Record
|
||||
|
||||
```typescript
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
await db.insert(Post).values({
|
||||
title: 'New Post',
|
||||
slug: 'new-post',
|
||||
content: 'Content here',
|
||||
authorId: 1,
|
||||
published: false
|
||||
});
|
||||
```
|
||||
|
||||
### Insert Multiple Records
|
||||
|
||||
```typescript
|
||||
await db.insert(Tag).values([
|
||||
{ name: 'Tag 1', slug: 'tag-1' },
|
||||
{ name: 'Tag 2', slug: 'tag-2' },
|
||||
{ name: 'Tag 3', slug: 'tag-3' }
|
||||
]);
|
||||
```
|
||||
|
||||
### Update Records
|
||||
|
||||
```typescript
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
// Update single field
|
||||
await db
|
||||
.update(Post)
|
||||
.set({ views: 100 })
|
||||
.where(eq(Post.id, 1));
|
||||
|
||||
// Update multiple fields
|
||||
await db
|
||||
.update(Post)
|
||||
.set({
|
||||
published: true,
|
||||
publishedAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(Post.slug, 'my-post'));
|
||||
```
|
||||
|
||||
### Delete Records
|
||||
|
||||
```typescript
|
||||
import { db, Post, Comment, eq, lt } from 'astro:db';
|
||||
|
||||
// Delete by ID
|
||||
await db
|
||||
.delete(Post)
|
||||
.where(eq(Post.id, 1));
|
||||
|
||||
// Delete with condition
|
||||
await db
|
||||
.delete(Comment)
|
||||
.where(lt(Comment.createdAt, new Date('2024-01-01')));
|
||||
```
|
||||
|
||||
## API Endpoint Patterns
|
||||
|
||||
### GET All Items
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts.json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, desc } from 'astro:db';
|
||||
|
||||
export const GET: APIRoute = async () => {
|
||||
const posts = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.orderBy(desc(Post.publishedAt));
|
||||
|
||||
return new Response(JSON.stringify(posts), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### GET Single Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts/[slug].json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
const post = await db
|
||||
.select()
|
||||
.from(Post)
|
||||
.where(eq(Post.slug, params.slug))
|
||||
.get();
|
||||
|
||||
if (!post) {
|
||||
return new Response(JSON.stringify({ error: 'Not found' }), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(post), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### POST Create Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts.json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post } from 'astro:db';
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const data = await request.json();
|
||||
|
||||
await db.insert(Post).values({
|
||||
title: data.title,
|
||||
slug: data.slug,
|
||||
content: data.content,
|
||||
authorId: data.authorId,
|
||||
published: false,
|
||||
createdAt: new Date()
|
||||
});
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### PUT/PATCH Update Item
|
||||
|
||||
```typescript
|
||||
// src/pages/api/posts/[id].json.ts
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db, Post, eq } from 'astro:db';
|
||||
|
||||
export const PUT: APIRoute = async ({ params, request }) => {
|
||||
const data = await request.json();
|
||||
const id = parseInt(params.id);
|
||||
|
||||
await db
|
||||
.update(Post)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(Post.id, id));
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### DELETE Item
|
||||
|
||||
```typescript
|
||||
export const DELETE: APIRoute = async ({ params }) => {
|
||||
const id = parseInt(params.id);
|
||||
|
||||
await db.delete(Post).where(eq(Post.id, id));
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```env
|
||||
# .env
|
||||
ASTRO_DB_REMOTE_URL=libsql://your-db.turso.io
|
||||
ASTRO_DB_APP_TOKEN=your-auth-token
|
||||
```
|
||||
|
||||
### Push Schema to Production
|
||||
|
||||
```bash
|
||||
astro db push --remote
|
||||
```
|
||||
|
||||
### Verify Remote Connection
|
||||
|
||||
```bash
|
||||
astro db verify --remote
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Primary Keys**: Always define a primary key for each table
|
||||
2. **Unique Constraints**: Use `unique: true` for fields like email, slug
|
||||
3. **Default Values**: Provide sensible defaults for boolean/number fields
|
||||
4. **Optional Fields**: Mark truly optional fields with `optional: true`
|
||||
5. **Indexing**: Use unique constraints for fields you'll query often
|
||||
6. **Relationships**: Use foreign keys (number columns) to link tables
|
||||
7. **Dates**: Always use `column.date()` for timestamp fields
|
||||
8. **JSON**: Use JSON columns for flexible/nested data
|
||||
9. **Naming**: Use consistent naming (camelCase for columns, PascalCase for tables)
|
||||
10. **Seeding**: Keep seed data representative and minimal
|
||||
|
||||
## Tips
|
||||
|
||||
- Dev server auto-restarts on schema changes
|
||||
- Check `.astro/content.db` for local database
|
||||
- Use `.get()` for single results, omit for arrays
|
||||
- Drizzle ORM provides type-safe queries
|
||||
- Test queries in seed file first
|
||||
- Use transactions for related inserts
|
||||
- Consider indexes for frequently queried fields
|
||||
- Migrate carefully in production
|
||||
584
skills/frontmatter-schemas/SKILL.md
Normal file
584
skills/frontmatter-schemas/SKILL.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# Astro Frontmatter Schemas Skill
|
||||
|
||||
Common Zod schema patterns and frontmatter examples for Astro content collections.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Designing content collection schemas
|
||||
- Creating frontmatter for markdown files
|
||||
- Validating content structure
|
||||
- Setting up type-safe content
|
||||
|
||||
## Common Schema Patterns
|
||||
|
||||
### Blog Post Schema
|
||||
|
||||
```typescript
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
|
||||
schema: z.object({
|
||||
// Core Fields
|
||||
title: z.string().max(80),
|
||||
description: z.string().max(160),
|
||||
pubDate: z.coerce.date(),
|
||||
updatedDate: z.coerce.date().optional(),
|
||||
|
||||
// Author
|
||||
author: z.string(),
|
||||
authorUrl: z.string().url().optional(),
|
||||
|
||||
// Categorization
|
||||
tags: z.array(z.string()).default([]),
|
||||
category: z.enum(['tutorial', 'news', 'guide', 'review']).optional(),
|
||||
|
||||
// Publishing
|
||||
draft: z.boolean().default(false),
|
||||
featured: z.boolean().default(false),
|
||||
|
||||
// Media
|
||||
image: z.object({
|
||||
url: z.string(),
|
||||
alt: z.string()
|
||||
}).optional(),
|
||||
|
||||
// SEO
|
||||
canonicalURL: z.string().url().optional(),
|
||||
|
||||
// Metadata
|
||||
readingTime: z.number().optional(),
|
||||
relatedPosts: z.array(z.string()).default([])
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Corresponding Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'Getting Started with Astro'
|
||||
description: 'Learn the fundamentals of building fast websites with Astro'
|
||||
pubDate: 2024-01-15
|
||||
updatedDate: 2024-01-20
|
||||
author: 'John Doe'
|
||||
authorUrl: 'https://example.com/john'
|
||||
tags: ['astro', 'tutorial', 'web-dev']
|
||||
category: 'tutorial'
|
||||
draft: false
|
||||
featured: true
|
||||
image:
|
||||
url: './images/hero.jpg'
|
||||
alt: 'Astro logo with stars'
|
||||
canonicalURL: 'https://example.com/blog/getting-started'
|
||||
readingTime: 8
|
||||
relatedPosts: ['advanced-astro', 'astro-tips']
|
||||
---
|
||||
```
|
||||
|
||||
### Documentation Schema
|
||||
|
||||
```typescript
|
||||
const docs = defineCollection({
|
||||
loader: glob({ pattern: "**/*.mdx", base: "./src/content/docs" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
|
||||
// Sidebar
|
||||
sidebar: z.object({
|
||||
order: z.number(),
|
||||
label: z.string().optional(),
|
||||
hidden: z.boolean().default(false),
|
||||
badge: z.string().optional()
|
||||
}).optional(),
|
||||
|
||||
// Status
|
||||
lastUpdated: z.coerce.date().optional(),
|
||||
version: z.string().optional(),
|
||||
deprecated: z.boolean().default(false),
|
||||
|
||||
// Contributors
|
||||
contributors: z.array(z.string()).default([]),
|
||||
|
||||
// Navigation
|
||||
prev: z.object({
|
||||
text: z.string(),
|
||||
link: z.string()
|
||||
}).optional(),
|
||||
next: z.object({
|
||||
text: z.string(),
|
||||
link: z.string()
|
||||
}).optional(),
|
||||
|
||||
// Table of Contents
|
||||
tableOfContents: z.boolean().default(true),
|
||||
tocDepth: z.number().min(1).max(6).default(3)
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Corresponding Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'API Reference'
|
||||
description: 'Complete API documentation for the core library'
|
||||
sidebar:
|
||||
order: 2
|
||||
label: 'API Docs'
|
||||
hidden: false
|
||||
badge: 'New'
|
||||
lastUpdated: 2024-01-20
|
||||
version: '2.1.0'
|
||||
deprecated: false
|
||||
contributors: ['john-doe', 'jane-smith']
|
||||
prev:
|
||||
text: 'Installation'
|
||||
link: '/docs/installation'
|
||||
next:
|
||||
text: 'Configuration'
|
||||
link: '/docs/configuration'
|
||||
tableOfContents: true
|
||||
tocDepth: 3
|
||||
---
|
||||
```
|
||||
|
||||
### Product Schema
|
||||
|
||||
```typescript
|
||||
const products = defineCollection({
|
||||
loader: glob({ pattern: "**/*.json", base: "./src/content/products" }),
|
||||
schema: z.object({
|
||||
// Basic Info
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
shortDescription: z.string().max(100).optional(),
|
||||
|
||||
// Pricing
|
||||
price: z.number().positive(),
|
||||
currency: z.string().default('USD'),
|
||||
salePrice: z.number().positive().optional(),
|
||||
|
||||
// Categorization
|
||||
category: z.enum(['software', 'hardware', 'service', 'digital']),
|
||||
tags: z.array(z.string()).default([]),
|
||||
|
||||
// Inventory
|
||||
sku: z.string(),
|
||||
inStock: z.boolean().default(true),
|
||||
quantity: z.number().int().min(0).optional(),
|
||||
|
||||
// Features
|
||||
features: z.array(z.string()),
|
||||
specifications: z.record(z.string()).optional(),
|
||||
|
||||
// Media
|
||||
images: z.array(z.object({
|
||||
url: z.string(),
|
||||
alt: z.string(),
|
||||
primary: z.boolean().default(false)
|
||||
})),
|
||||
|
||||
// Rating
|
||||
rating: z.number().min(0).max(5).optional(),
|
||||
reviewCount: z.number().int().min(0).default(0)
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Corresponding JSON:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Premium Web Hosting",
|
||||
"description": "Fast, reliable web hosting with 99.9% uptime",
|
||||
"shortDescription": "Premium hosting solution",
|
||||
"price": 29.99,
|
||||
"currency": "USD",
|
||||
"salePrice": 19.99,
|
||||
"category": "service",
|
||||
"tags": ["hosting", "cloud", "premium"],
|
||||
"sku": "HOST-001",
|
||||
"inStock": true,
|
||||
"quantity": 100,
|
||||
"features": [
|
||||
"99.9% uptime guarantee",
|
||||
"Free SSL certificate",
|
||||
"24/7 support"
|
||||
],
|
||||
"specifications": {
|
||||
"storage": "100GB SSD",
|
||||
"bandwidth": "Unlimited",
|
||||
"domains": "1"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"url": "/images/hosting-main.jpg",
|
||||
"alt": "Server rack",
|
||||
"primary": true
|
||||
}
|
||||
],
|
||||
"rating": 4.5,
|
||||
"reviewCount": 127
|
||||
}
|
||||
```
|
||||
|
||||
### Team Member Schema
|
||||
|
||||
```typescript
|
||||
const team = defineCollection({
|
||||
loader: glob({ pattern: "**/*.yml", base: "./src/content/team" }),
|
||||
schema: z.object({
|
||||
name: z.string(),
|
||||
role: z.string(),
|
||||
department: z.enum(['engineering', 'design', 'marketing', 'sales']).optional(),
|
||||
|
||||
bio: z.string(),
|
||||
|
||||
avatar: z.string(),
|
||||
|
||||
social: z.object({
|
||||
twitter: z.string().url().optional(),
|
||||
github: z.string().url().optional(),
|
||||
linkedin: z.string().url().optional(),
|
||||
website: z.string().url().optional()
|
||||
}).optional(),
|
||||
|
||||
startDate: z.coerce.date(),
|
||||
location: z.string().optional(),
|
||||
|
||||
skills: z.array(z.string()).default([]),
|
||||
featured: z.boolean().default(false)
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Corresponding YAML:**
|
||||
|
||||
```yaml
|
||||
name: 'Jane Smith'
|
||||
role: 'Senior Software Engineer'
|
||||
department: 'engineering'
|
||||
bio: 'Full-stack developer with 10 years experience building scalable web applications.'
|
||||
avatar: '/images/team/jane.jpg'
|
||||
social:
|
||||
twitter: 'https://twitter.com/janesmith'
|
||||
github: 'https://github.com/janesmith'
|
||||
linkedin: 'https://linkedin.com/in/janesmith'
|
||||
website: 'https://janesmith.dev'
|
||||
startDate: 2020-03-15
|
||||
location: 'San Francisco, CA'
|
||||
skills: ['TypeScript', 'React', 'Node.js', 'AWS']
|
||||
featured: true
|
||||
```
|
||||
|
||||
### Project/Portfolio Schema
|
||||
|
||||
```typescript
|
||||
const projects = defineCollection({
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/projects" }),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
|
||||
technologies: z.array(z.string()),
|
||||
|
||||
liveUrl: z.string().url().optional(),
|
||||
githubUrl: z.string().url().optional(),
|
||||
demoUrl: z.string().url().optional(),
|
||||
|
||||
thumbnail: z.string(),
|
||||
images: z.array(z.string()).default([]),
|
||||
|
||||
date: z.coerce.date(),
|
||||
endDate: z.coerce.date().optional(),
|
||||
|
||||
status: z.enum(['active', 'completed', 'archived']).default('active'),
|
||||
featured: z.boolean().default(false),
|
||||
|
||||
client: z.string().optional(),
|
||||
team: z.array(z.string()).default([]),
|
||||
|
||||
tags: z.array(z.string()).default([])
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**Corresponding Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'E-commerce Platform Redesign'
|
||||
description: 'Complete redesign and modernization of legacy e-commerce platform'
|
||||
technologies: ['React', 'Next.js', 'TypeScript', 'Tailwind CSS', 'PostgreSQL']
|
||||
liveUrl: 'https://shop.example.com'
|
||||
githubUrl: 'https://github.com/example/shop'
|
||||
thumbnail: './images/shop-thumb.jpg'
|
||||
images:
|
||||
- './images/shop-1.jpg'
|
||||
- './images/shop-2.jpg'
|
||||
- './images/shop-3.jpg'
|
||||
date: 2023-06-01
|
||||
endDate: 2023-12-15
|
||||
status: 'completed'
|
||||
featured: true
|
||||
client: 'Example Corp'
|
||||
team: ['john-doe', 'jane-smith']
|
||||
tags: ['web-dev', 'e-commerce', 'react']
|
||||
---
|
||||
```
|
||||
|
||||
## Zod Validation Patterns
|
||||
|
||||
### String Validation
|
||||
|
||||
```typescript
|
||||
// Basic string
|
||||
title: z.string()
|
||||
|
||||
// Length constraints
|
||||
title: z.string().min(1).max(100)
|
||||
description: z.string().max(500)
|
||||
|
||||
// Email validation
|
||||
email: z.string().email()
|
||||
|
||||
// URL validation
|
||||
website: z.string().url()
|
||||
|
||||
// Regex pattern
|
||||
slug: z.string().regex(/^[a-z0-9-]+$/)
|
||||
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)
|
||||
|
||||
// Specific values
|
||||
status: z.string().refine(val => ['draft', 'published'].includes(val))
|
||||
```
|
||||
|
||||
### Number Validation
|
||||
|
||||
```typescript
|
||||
// Basic number
|
||||
count: z.number()
|
||||
|
||||
// Integer only
|
||||
age: z.number().int()
|
||||
|
||||
// Positive numbers
|
||||
price: z.number().positive()
|
||||
|
||||
// Range
|
||||
rating: z.number().min(1).max(5)
|
||||
percentage: z.number().min(0).max(100)
|
||||
|
||||
// With coercion (converts strings)
|
||||
views: z.coerce.number()
|
||||
```
|
||||
|
||||
### Date Validation
|
||||
|
||||
```typescript
|
||||
// With coercion (recommended)
|
||||
pubDate: z.coerce.date()
|
||||
|
||||
// Strict date object
|
||||
deadline: z.date()
|
||||
|
||||
// Date range
|
||||
startDate: z.coerce.date()
|
||||
endDate: z.coerce.date().refine(
|
||||
(date) => date > startDate,
|
||||
"End date must be after start date"
|
||||
)
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
```typescript
|
||||
// Array of strings
|
||||
tags: z.array(z.string())
|
||||
|
||||
// Minimum items
|
||||
tags: z.array(z.string()).min(1)
|
||||
|
||||
// Maximum items
|
||||
images: z.array(z.string()).max(10)
|
||||
|
||||
// Default value
|
||||
tags: z.array(z.string()).default([])
|
||||
|
||||
// Array of objects
|
||||
authors: z.array(z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email()
|
||||
}))
|
||||
```
|
||||
|
||||
### Object Validation
|
||||
|
||||
```typescript
|
||||
// Nested object
|
||||
author: z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
url: z.string().url().optional()
|
||||
})
|
||||
|
||||
// Record (dynamic keys)
|
||||
metadata: z.record(z.string())
|
||||
settings: z.record(z.boolean())
|
||||
|
||||
// Partial object (all fields optional)
|
||||
options: z.object({
|
||||
theme: z.string(),
|
||||
color: z.string()
|
||||
}).partial()
|
||||
```
|
||||
|
||||
### Enum Validation
|
||||
|
||||
```typescript
|
||||
// Fixed set of values
|
||||
status: z.enum(['draft', 'published', 'archived'])
|
||||
priority: z.enum(['low', 'medium', 'high'])
|
||||
category: z.enum(['news', 'tutorial', 'guide'])
|
||||
```
|
||||
|
||||
### Boolean Validation
|
||||
|
||||
```typescript
|
||||
// Basic boolean
|
||||
published: z.boolean()
|
||||
|
||||
// With default
|
||||
draft: z.boolean().default(false)
|
||||
featured: z.boolean().default(false)
|
||||
```
|
||||
|
||||
### Optional and Default Values
|
||||
|
||||
```typescript
|
||||
// Optional field
|
||||
subtitle: z.string().optional()
|
||||
updatedDate: z.coerce.date().optional()
|
||||
|
||||
// With default value
|
||||
draft: z.boolean().default(false)
|
||||
views: z.number().default(0)
|
||||
tags: z.array(z.string()).default([])
|
||||
```
|
||||
|
||||
### Union Types
|
||||
|
||||
```typescript
|
||||
// Multiple possible types
|
||||
content: z.union([
|
||||
z.string(),
|
||||
z.object({ html: z.string() })
|
||||
])
|
||||
|
||||
// Discriminated union
|
||||
media: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('image'),
|
||||
url: z.string(),
|
||||
alt: z.string()
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('video'),
|
||||
url: z.string(),
|
||||
duration: z.number()
|
||||
})
|
||||
])
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use z.coerce.date()** for date strings in frontmatter
|
||||
2. **Provide defaults** for booleans and arrays
|
||||
3. **Use enums** for fixed sets of values
|
||||
4. **Validate formats** with built-in validators (email, url)
|
||||
5. **Add constraints** (min, max) for better validation
|
||||
6. **Use optional()** for truly optional fields
|
||||
7. **Document schemas** with TypeScript comments
|
||||
8. **Keep schemas DRY** by extracting common patterns
|
||||
|
||||
## Common Frontmatter Patterns
|
||||
|
||||
### Minimal Blog Post
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'Post Title'
|
||||
description: 'Post description'
|
||||
pubDate: 2024-01-15
|
||||
author: 'Author Name'
|
||||
tags: ['tag1', 'tag2']
|
||||
---
|
||||
```
|
||||
|
||||
### Full-Featured Blog Post
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'Comprehensive Post Title'
|
||||
description: 'Detailed SEO-optimized description'
|
||||
pubDate: 2024-01-15
|
||||
updatedDate: 2024-01-20
|
||||
author: 'Author Name'
|
||||
authorUrl: 'https://author.com'
|
||||
tags: ['web-dev', 'tutorial']
|
||||
category: 'tutorial'
|
||||
draft: false
|
||||
featured: true
|
||||
image:
|
||||
url: './hero.jpg'
|
||||
alt: 'Hero image description'
|
||||
canonicalURL: 'https://example.com/original'
|
||||
readingTime: 10
|
||||
relatedPosts: ['post-1', 'post-2']
|
||||
---
|
||||
```
|
||||
|
||||
### Documentation Page
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'Getting Started'
|
||||
description: 'Introduction to the framework'
|
||||
sidebar:
|
||||
order: 1
|
||||
label: 'Intro'
|
||||
lastUpdated: 2024-01-15
|
||||
contributors: ['john', 'jane']
|
||||
tableOfContents: true
|
||||
---
|
||||
```
|
||||
|
||||
### Product (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Product Name",
|
||||
"description": "Product description",
|
||||
"price": 99.99,
|
||||
"category": "software",
|
||||
"sku": "PROD-001",
|
||||
"inStock": true,
|
||||
"features": ["Feature 1", "Feature 2"],
|
||||
"images": [
|
||||
{ "url": "/img.jpg", "alt": "Product", "primary": true }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Always match frontmatter to schema exactly
|
||||
- Use ISO date format: `2024-01-15`
|
||||
- Quote strings with special characters
|
||||
- Use arrays for multiple values: `['tag1', 'tag2']`
|
||||
- Nest objects with proper indentation
|
||||
- Test with sample content first
|
||||
- Check generated types in `.astro/types.d.ts`
|
||||
Reference in New Issue
Block a user