New and Updated: ConvexFS Media Library with Bunny CDN integration ,OpenCode AI development tool integration, AI image generation download and copy options

This commit is contained in:
Wayne Sutton
2026-01-10 15:53:27 -08:00
parent d5d8de0058
commit 95cc8a4677
43 changed files with 5941 additions and 526 deletions

View File

@@ -0,0 +1,96 @@
---
description: Content creation specialist for posts and pages
mode: subagent
model: claude-sonnet-4-20250514
tools:
write: true
edit: true
bash: false
---
# Content Writer Agent
You are a content creation specialist for the markdown publishing framework.
## Responsibilities
1. Create new blog posts in `content/blog/`
2. Create new pages in `content/pages/`
3. Edit existing content
4. Validate frontmatter
5. Suggest when to run sync
## Creating a Blog Post
Location: `content/blog/{slug}.md`
Required frontmatter:
```yaml
---
title: "Post Title"
description: "SEO description"
date: "YYYY-MM-DD"
slug: "url-slug"
published: true
tags: ["tag1", "tag2"]
---
```
Optional fields: featured, featuredOrder, image, showImageAtTop, excerpt, readTime, authorName, authorImage, layout, rightSidebar, aiChat, blogFeatured, newsletter, contactForm, unlisted, showFooter, footer, showSocialFooter
## Creating a Page
Location: `content/pages/{slug}.md`
Required frontmatter:
```yaml
---
title: "Page Title"
slug: "url-slug"
published: true
---
```
Optional fields: order, showInNav, featured, featuredOrder, image, showImageAtTop, excerpt, authorName, authorImage, layout, rightSidebar, aiChat, contactForm, newsletter, textAlign, showFooter, footer, showSocialFooter
## Docs Navigation
To include content in the docs sidebar:
```yaml
docsSection: true
docsSectionGroup: "Group Name"
docsSectionOrder: 1
docsSectionGroupOrder: 1
docsSectionGroupIcon: "Rocket"
```
## Validation Checklist
Before creating content:
- [ ] Slug is unique (not used by any other post/page)
- [ ] Date is in YYYY-MM-DD format
- [ ] published is boolean (true/false)
- [ ] tags is an array (for posts)
- [ ] Required fields are present
## After Creating Content
Always remind the user to run:
```bash
npm run sync # Development
npm run sync:prod # Production
```
Or use the `/sync` command.
## Writing Guidelines
- No emojis unless requested
- No em dashes between words
- Sentence case for headings
- Keep descriptions under 160 characters for SEO

View File

@@ -0,0 +1,72 @@
---
description: Main orchestrator for markdown publishing framework
mode: primary
model: claude-sonnet-4-20250514
tools:
write: true
edit: true
bash: true
---
# Orchestrator Agent
You are the main orchestrator for a markdown publishing framework built with React, Vite, and Convex.
## Workflow
Follow this structured approach:
1. **Understand** - Analyze the user's request
2. **Plan** - Determine which specialist agent or action is needed
3. **Delegate** - Route to the appropriate agent or execute directly
4. **Verify** - Check that the task completed successfully
5. **Report** - Summarize what was done
## Routing Rules
**Content creation tasks** (new posts, pages, writing):
- Delegate to @content-writer agent
**Sync and deployment tasks** (sync, deploy, environment):
- Delegate to @sync-manager agent
**Code changes** (components, functions, styling):
- Handle directly or use default code capabilities
## Key Commands
Quick commands available via `/` prefix:
| Command | Purpose |
|---------|---------|
| `/sync` | Sync content to development |
| `/sync-prod` | Sync content to production |
| `/create-post` | Create a new blog post |
| `/create-page` | Create a new page |
| `/import` | Import content from URL |
| `/deploy` | Deploy to production |
## Project Structure
- `content/blog/` - Markdown blog posts
- `content/pages/` - Static pages
- `convex/` - Backend functions
- `src/` - React frontend
- `scripts/` - Sync and utility scripts
## Skills Reference
Use these skills for detailed documentation:
- **frontmatter** - Frontmatter syntax for posts/pages
- **sync** - How the sync system works
- **convex** - Convex patterns and conventions
- **content** - Content management guide
## Important Rules
1. Never break existing functionality
2. Always validate frontmatter before creating content
3. Run sync after content changes
4. Use indexes in Convex queries (never .filter())
5. No emojis unless explicitly requested

View File

@@ -0,0 +1,105 @@
---
description: Sync and deployment specialist
mode: subagent
model: claude-sonnet-4-20250514
tools:
write: false
edit: false
bash: true
---
# Sync Manager Agent
You are the sync and deployment specialist for the markdown publishing framework.
## Responsibilities
1. Execute sync commands
2. Manage development vs production environments
3. Handle deployments
4. Troubleshoot sync issues
5. Import content from URLs
## Sync Commands
| Command | Environment | Purpose |
|---------|-------------|---------|
| `npm run sync` | Development | Sync markdown to dev Convex |
| `npm run sync:prod` | Production | Sync markdown to prod Convex |
| `npm run sync:discovery` | Development | Update AGENTS.md, llms.txt |
| `npm run sync:discovery:prod` | Production | Update discovery files |
| `npm run sync:all` | Development | Sync everything |
| `npm run sync:all:prod` | Production | Sync everything |
## Import External Content
```bash
npm run import https://example.com/article
```
Requires FIRECRAWL_API_KEY in `.env.local`. After import, run sync.
## Export Dashboard Content
```bash
npm run export:db # Development
npm run export:db:prod # Production
```
Exports dashboard-created content to markdown files.
## Environment Files
| File | Purpose |
|------|---------|
| `.env.local` | Development Convex URL |
| `.env.production.local` | Production Convex URL |
## Deployment Workflow
1. Sync content to production:
```bash
npm run sync:all:prod
```
2. Deploy Convex functions:
```bash
npx convex deploy
```
3. Build and deploy frontend (Netlify handles automatically)
## Troubleshooting
### "VITE_CONVEX_URL not set"
Run `npx convex dev` first to create `.env.local`.
### Posts not appearing
1. Check `published: true` in frontmatter
2. Verify required fields
3. Check Convex dashboard for errors
4. Run sync again
### Sync to wrong environment
Check which command you ran:
- `npm run sync` = development
- `npm run sync:prod` = production
## Sync Server
The project includes a local sync server at `localhost:3001` for Dashboard integration:
- Start: `npm run sync-server`
- Endpoint: POST `/api/sync` with `{ command: "sync" }`
- Health: GET `/health`
## Verification
After any sync, verify:
1. Content appears on the site
2. No errors in terminal output
3. Convex dashboard shows updated records

View File

@@ -0,0 +1,70 @@
---
description: Create a new static page with proper frontmatter
---
# /create-page
Creates a new static page in `content/pages/` with validated frontmatter.
## Workflow
1. Ask for page details (title, slug)
2. Validate slug uniqueness
3. Create the markdown file with frontmatter
4. Remind to run sync
## Required information
| Field | Description |
|-------|-------------|
| title | Page title |
| slug | URL path (must be unique) |
## Optional information
| Field | Description |
|-------|-------------|
| order | Navigation order (lower = first) |
| showInNav | Show in navigation menu (default: true) |
| featured | Show in featured section |
| excerpt | Short text for cards |
| layout | "sidebar" for docs-style |
## File template
```markdown
---
title: "{title}"
slug: "{slug}"
published: true
order: {order}
showInNav: true
---
{content}
```
## Special pages
| Page | Slug | Purpose |
|------|------|---------|
| Homepage intro | home-intro | Content shown on homepage |
| Footer | footer | Footer content |
## After creation
Run sync to publish:
```bash
npm run sync
```
## For docs navigation
Add these fields to include in docs sidebar:
```yaml
docsSection: true
docsSectionGroup: "Group Name"
docsSectionOrder: 1
```

View File

@@ -0,0 +1,60 @@
---
description: Create a new blog post with proper frontmatter
---
# /create-post
Creates a new blog post in `content/blog/` with validated frontmatter.
## Workflow
1. Ask for post details (title, description, tags)
2. Generate a URL-safe slug
3. Create the markdown file with frontmatter
4. Remind to run sync
## Required information
| Field | Description |
|-------|-------------|
| title | Post title |
| description | SEO description (under 160 chars) |
| tags | Array of topic tags |
## Optional information
| Field | Description |
|-------|-------------|
| image | Header/OG image path |
| featured | Show in featured section |
| excerpt | Short text for cards |
| authorName | Author display name |
## File template
```markdown
---
title: "{title}"
description: "{description}"
date: "{YYYY-MM-DD}"
slug: "{slug}"
published: true
tags: [{tags}]
---
{content}
```
## After creation
Run sync to publish:
```bash
npm run sync
```
## Validation
- Slug must be unique across all posts/pages
- Date must be YYYY-MM-DD format
- Tags must be an array

View File

@@ -0,0 +1,70 @@
---
description: Deploy changes to production
---
# /deploy
Full deployment workflow for pushing changes to production.
## Deployment steps
### 1. Sync content to production
```bash
npm run sync:all:prod
```
This syncs:
- All posts and pages
- Discovery files (AGENTS.md, llms.txt)
- Raw markdown files
### 2. Deploy Convex functions
```bash
npx convex deploy
```
This pushes any changes to:
- Mutations and queries
- Schema changes
- HTTP endpoints
- Cron jobs
### 3. Build and deploy frontend
If using Netlify (automatic):
- Push to main branch triggers build
- Netlify runs: `npm ci --include=dev && npx convex deploy --cmd 'npm run build'`
If manual:
```bash
npm run build
```
## Verification checklist
After deployment:
- [ ] Production site loads correctly
- [ ] New content appears
- [ ] Existing content still works
- [ ] No console errors
- [ ] RSS feed updates
- [ ] Sitemap includes new pages
## Environment requirements
| File | Purpose |
|------|---------|
| `.env.production.local` | Production Convex URL |
| Netlify env vars | API keys, Convex deployment |
## Rollback
If something goes wrong:
1. Check Convex dashboard for function errors
2. Redeploy previous Convex version if needed
3. Check Netlify for build logs
4. Trigger a redeploy in Netlify dashboard

View File

@@ -0,0 +1,62 @@
---
description: Import external URL content as a markdown post
---
# /import
Imports content from an external URL and creates a new blog post.
## Usage
```bash
npm run import https://example.com/article
```
## Requirements
- `FIRECRAWL_API_KEY` in `.env.local`
- Valid, accessible URL
## What it does
1. Fetches the URL via Firecrawl API
2. Converts HTML to clean markdown
3. Extracts metadata (title, description)
4. Generates frontmatter
5. Creates file in `content/blog/`
## After import
You still need to run sync:
```bash
npm run sync
```
## Editing imported content
After import, you can edit the generated file in `content/blog/` to:
- Adjust the title
- Update the description
- Add/remove tags
- Edit the content
- Add images
## Troubleshooting
### "FIRECRAWL_API_KEY not set"
Add to your `.env.local`:
```
FIRECRAWL_API_KEY=your_api_key_here
```
### Content looks wrong
Some sites may not convert cleanly. Edit the generated markdown manually.
### Import failed
Check if the URL is accessible and not blocked by robots.txt or authentication.

View File

@@ -0,0 +1,44 @@
---
description: Sync markdown content to production Convex database
---
# /sync-prod
Syncs all markdown content to the production Convex database.
## What it does
Same as `/sync` but targets the production environment using `.env.production.local`.
## Usage
```bash
npm run sync:prod
```
## Requirements
- `.env.production.local` file must exist with `VITE_CONVEX_URL`
- Production Convex deployment must be configured
## When to use
- Before deploying changes to production
- When updating live content
- As part of the deployment workflow
## Full production sync
To sync everything including discovery files:
```bash
npm run sync:all:prod
```
## Verification
After production sync:
1. Check your production site
2. Verify content appears correctly
3. Check Convex dashboard for the production deployment

42
.opencode/command/sync.md Normal file
View File

@@ -0,0 +1,42 @@
---
description: Sync markdown content to development Convex database
---
# /sync
Syncs all markdown content from `content/blog/` and `content/pages/` to the development Convex database.
## What it does
1. Reads all `.md` files from content directories
2. Parses frontmatter with gray-matter
3. Validates required fields
4. Calculates reading time if not provided
5. Upserts content to Convex database
6. Generates raw markdown files in `public/raw/`
## Usage
```bash
npm run sync
```
## When to use
- After creating or editing markdown files
- After importing content from URLs
- When content is not appearing on the site
## Output
The command shows:
- Number of posts synced
- Number of pages synced
- Any validation warnings
- Generated raw files
## Next steps
After syncing, visit `http://localhost:5173` to see your content.
For production sync, use `/sync-prod` instead.

6
.opencode/config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://opencode.ai/schemas/config.json",
"name": "markdown-publishing-framework",
"description": "AI-first markdown publishing with Convex real-time sync. Write markdown, run sync, content appears instantly.",
"version": "1.0.0"
}

View File

@@ -0,0 +1,47 @@
/**
* Sync Helper Plugin for OpenCode
*
* Provides helpful reminders when content files are edited.
* Minimal implementation - just logging, no auto-sync.
*/
import type { Plugin } from "@opencode-ai/plugin";
export const SyncHelper: Plugin = async ({ client }) => {
// Track if we've reminded recently to avoid spam
let lastReminder = 0;
const REMINDER_COOLDOWN = 30000; // 30 seconds
return {
hooks: {
// When a file is edited
"file.edited": async (event) => {
const path = event.path;
// Check if it's a content file
if (
path.startsWith("content/blog/") ||
path.startsWith("content/pages/")
) {
const now = Date.now();
// Only remind if cooldown has passed
if (now - lastReminder > REMINDER_COOLDOWN) {
lastReminder = now;
await client.app.log(
"info",
`Content changed: ${path} - Run /sync to publish`
);
}
}
},
// When session becomes idle
"session.idle": async () => {
// Could add pending sync detection here in the future
},
},
};
};
export default SyncHelper;

187
.opencode/skill/content.md Normal file
View File

@@ -0,0 +1,187 @@
---
description: Content management guide
---
# Content Management Guide
How to manage content in the markdown publishing framework.
## Content locations
| Type | Location | Purpose |
|------|----------|---------|
| Blog posts | `content/blog/*.md` | Time-based articles |
| Pages | `content/pages/*.md` | Static pages |
| Raw files | `public/raw/*.md` | Generated static copies |
| Images | `public/images/` | Static images |
## Creating content
### New blog post
1. Create file: `content/blog/my-post.md`
2. Add required frontmatter:
```yaml
---
title: "My Post Title"
description: "SEO description"
date: "2025-01-15"
slug: "my-post"
published: true
tags: ["topic"]
---
```
3. Write content in markdown
4. Run `npm run sync`
5. View at `localhost:5173/my-post`
### New page
1. Create file: `content/pages/my-page.md`
2. Add required frontmatter:
```yaml
---
title: "My Page"
slug: "my-page"
published: true
---
```
3. Write content
4. Run `npm run sync`
5. View at `localhost:5173/my-page`
## Special pages
| Slug | Purpose |
|------|---------|
| `home-intro` | Homepage intro content |
| `footer` | Footer content |
These render in special locations. Set `showInNav: false` to hide from navigation.
## Content workflow
```
Write markdown --> npm run sync --> Convex DB --> Site
```
### Development
1. Edit markdown files
2. Run `npm run sync`
3. View changes at `localhost:5173`
### Production
1. Edit markdown files
2. Run `npm run sync:prod`
3. View changes on production site
## Images
### Local images
Place in `public/images/` and reference:
```markdown
![Alt text](/images/my-image.png)
```
### External images
Use full URLs:
```markdown
![Alt text](https://example.com/image.png)
```
### Image in frontmatter
```yaml
image: "/images/og-image.png"
showImageAtTop: true
```
## Markdown features
### Code blocks
````markdown
```typescript
const example = "code";
```
````
### Tables
```markdown
| Header | Header |
|--------|--------|
| Cell | Cell |
```
### Links
```markdown
[Link text](/internal-path)
[External](https://example.com)
```
### Headings
```markdown
# H1 (demoted to H2 in posts)
## H2
### H3
```
## Docs section
To add content to the docs sidebar:
```yaml
docsSection: true
docsSectionGroup: "Group Name"
docsSectionOrder: 1
```
## Unlisted content
To hide from listings but keep accessible:
```yaml
published: true
unlisted: true
```
## Draft content
To hide completely:
```yaml
published: false
```
## Import from URL
```bash
npm run import https://example.com/article
npm run sync
```
## Export from Dashboard
```bash
npm run export:db # Dev
npm run export:db:prod # Prod
```
Exports dashboard-created content to markdown files.
## Best practices
1. **Unique slugs** - Every post/page needs a unique slug
2. **SEO descriptions** - Keep under 160 characters
3. **Tags** - Use consistent tag names
4. **Images** - Optimize before uploading
5. **Sync after changes** - Always run sync

192
.opencode/skill/convex.md Normal file
View File

@@ -0,0 +1,192 @@
---
description: Convex patterns and conventions
---
# Convex Patterns Reference
Convex patterns and conventions for this markdown publishing framework.
## Function structure
Every Convex function needs argument and return validators:
```typescript
import { query } from "./_generated/server";
import { v } from "convex/values";
export const myQuery = query({
args: { slug: v.string() },
returns: v.union(v.object({...}), v.null()),
handler: async (ctx, args) => {
// implementation
},
});
```
If a function returns nothing, use `returns: v.null()` and `return null;`.
## Always use indexes
Never use `.filter()` on queries. Define indexes in schema and use `.withIndex()`:
```typescript
// Good
const post = await ctx.db
.query("posts")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
// Bad - causes table scans
const post = await ctx.db
.query("posts")
.filter((q) => q.eq(q.field("slug"), args.slug))
.first();
```
## Make mutations idempotent
Mutations should be safe to call multiple times:
```typescript
export const heartbeat = mutation({
args: { sessionId: v.string(), currentPath: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const now = Date.now();
const existing = await ctx.db
.query("activeSessions")
.withIndex("by_sessionId", (q) => q.eq("sessionId", args.sessionId))
.first();
if (existing) {
// Early return if recently updated with same data
if (existing.currentPath === args.currentPath &&
now - existing.lastSeen < 10000) {
return null;
}
await ctx.db.patch(existing._id, {
currentPath: args.currentPath,
lastSeen: now
});
return null;
}
await ctx.db.insert("activeSessions", { ...args, lastSeen: now });
return null;
},
});
```
## Patch directly without reading
When you only need to update fields, patch directly:
```typescript
// Good - patch directly
await ctx.db.patch(args.id, { content: args.content });
// Bad - unnecessary read creates conflict window
const doc = await ctx.db.get(args.id);
if (!doc) throw new Error("Not found");
await ctx.db.patch(args.id, { content: args.content });
```
## Use event records for counters
Never increment counters on documents. Use separate event records:
```typescript
// Good - insert event record
await ctx.db.insert("pageViews", {
path,
sessionId,
timestamp: Date.now()
});
// Bad - counter updates cause write conflicts
await ctx.db.patch(pageId, { views: page.views + 1 });
```
## Schema indexes in this project
Key indexes defined in `convex/schema.ts`:
```typescript
posts: defineTable({...})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
.searchIndex("search_title", { searchField: "title" })
.searchIndex("search_content", { searchField: "content" })
pages: defineTable({...})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"])
```
## Common query patterns
### Get post by slug
```typescript
const post = await ctx.db
.query("posts")
.withIndex("by_slug", (q) => q.eq("slug", args.slug))
.first();
```
### Get all published posts
```typescript
const posts = await ctx.db
.query("posts")
.withIndex("by_published", (q) => q.eq("published", true))
.order("desc")
.collect();
```
### Get featured items
```typescript
const featured = await ctx.db
.query("posts")
.withIndex("by_featured", (q) => q.eq("featured", true))
.collect();
```
### Full text search
```typescript
const results = await ctx.db
.query("posts")
.withSearchIndex("search_content", (q) => q.search("content", searchTerm))
.take(10);
```
## File locations
| File | Purpose |
|------|---------|
| `convex/schema.ts` | Database schema and indexes |
| `convex/posts.ts` | Post queries/mutations |
| `convex/pages.ts` | Page queries/mutations |
| `convex/stats.ts` | Analytics (heartbeat, pageViews) |
| `convex/search.ts` | Full text search |
| `convex/http.ts` | HTTP endpoints |
| `convex/rss.ts` | RSS feed generation |
| `convex/crons.ts` | Scheduled jobs |
## Write conflict prevention
This project uses specific patterns to avoid write conflicts:
**Backend (convex/stats.ts):**
- 10-second dedup window for heartbeats
- Early return when session was recently updated
- Indexed queries for efficient lookups
**Frontend (src/hooks/usePageTracking.ts):**
- 5-second debounce window using refs
- Pending state tracking prevents overlapping calls
- Path tracking skips redundant heartbeats

View File

@@ -0,0 +1,194 @@
---
description: Frontmatter syntax for posts and pages
---
# Frontmatter Reference
How to write frontmatter for markdown-blog posts and pages.
## Blog post frontmatter
Location: `content/blog/*.md`
### Required fields
| Field | Type | Description |
|-------|------|-------------|
| title | string | Post title |
| description | string | SEO description |
| date | string | YYYY-MM-DD format |
| slug | string | URL path (must be unique) |
| published | boolean | true to show publicly |
| tags | string[] | Array of topic tags |
### Optional fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| featured | boolean | false | Show in featured section |
| featuredOrder | number | - | Display order (lower = first) |
| image | string | - | OG/header image path |
| showImageAtTop | boolean | false | Display image at top of post |
| excerpt | string | - | Short text for card view |
| readTime | string | auto | Reading time (auto-calculated if omitted) |
| authorName | string | - | Author display name |
| authorImage | string | - | Author avatar URL (round) |
| layout | string | - | "sidebar" for docs-style layout |
| rightSidebar | boolean | true | Show right sidebar |
| aiChat | boolean | false | Enable AI chat in sidebar |
| blogFeatured | boolean | false | Hero featured on /blog page |
| newsletter | boolean | - | Override newsletter signup |
| contactForm | boolean | false | Enable contact form |
| unlisted | boolean | false | Hide from listings but allow direct access via slug |
| showFooter | boolean | - | Override footer display |
| footer | string | - | Custom footer markdown |
| showSocialFooter | boolean | - | Override social footer |
### Example blog post
```markdown
---
title: "How to write a blog post"
description: "A guide to writing posts with frontmatter"
date: "2025-01-15"
slug: "how-to-write-a-blog-post"
published: true
tags: ["tutorial", "markdown"]
featured: true
featuredOrder: 1
image: "/images/my-post.png"
excerpt: "Learn how to create blog posts"
authorName: "Your Name"
authorImage: "/images/authors/you.png"
---
Your content here...
```
## Page frontmatter
Location: `content/pages/*.md`
### Required fields
| Field | Type | Description |
|-------|------|-------------|
| title | string | Page title |
| slug | string | URL path (must be unique) |
| published | boolean | true to show publicly |
### Optional fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| order | number | - | Nav order (lower = first) |
| showInNav | boolean | true | Show in navigation menu |
| featured | boolean | false | Show in featured section |
| featuredOrder | number | - | Display order (lower = first) |
| image | string | - | Thumbnail/OG image for cards |
| showImageAtTop | boolean | false | Display image at top |
| excerpt | string | - | Short text for card view |
| authorName | string | - | Author display name |
| authorImage | string | - | Author avatar URL |
| layout | string | - | "sidebar" for docs-style |
| rightSidebar | boolean | true | Show right sidebar |
| aiChat | boolean | false | Enable AI chat |
| contactForm | boolean | false | Enable contact form |
| newsletter | boolean | - | Override newsletter signup |
| textAlign | string | "left" | "left", "center", "right" |
| showFooter | boolean | - | Override footer display |
| footer | string | - | Custom footer markdown |
| showSocialFooter | boolean | - | Override social footer |
### Example page
```markdown
---
title: "About"
slug: "about"
published: true
order: 1
showInNav: true
featured: true
featuredOrder: 2
excerpt: "Learn about this site"
---
Page content here...
```
## Common patterns
### Featured post on homepage
```yaml
featured: true
featuredOrder: 1
```
### Hero post on /blog page
```yaml
blogFeatured: true
image: "/images/hero.png"
```
### Docs-style page with sidebar
```yaml
layout: "sidebar"
rightSidebar: true
```
### Hide from navigation
```yaml
showInNav: false
```
### Unlisted post
```yaml
published: true
unlisted: true
```
## Docs section navigation
Posts and pages can appear in the docs sidebar:
| Field | Type | Description |
|-------|------|-------------|
| docsSection | boolean | Include in docs navigation |
| docsSectionGroup | string | Sidebar group name |
| docsSectionOrder | number | Order within group |
| docsSectionGroupOrder | number | Order of group itself |
| docsSectionGroupIcon | string | Phosphor icon name |
| docsLanding | boolean | Use as /docs landing page |
### Example docs post
```yaml
---
title: "Getting Started"
slug: "getting-started"
published: true
docsSection: true
docsSectionGroup: "Quick Start"
docsSectionOrder: 1
docsSectionGroupOrder: 1
docsSectionGroupIcon: "Rocket"
layout: "sidebar"
---
```
## Validation
The sync script validates:
- Required fields must be present
- `slug` must be unique
- `date` should be YYYY-MM-DD format
- `published` must be boolean
Missing required fields will cause the file to be skipped with a warning.

126
.opencode/skill/sync.md Normal file
View File

@@ -0,0 +1,126 @@
---
description: How the content sync system works
---
# Sync System Reference
How the content sync system works in this markdown publishing framework.
## Overview
Content flows from markdown files to Convex database via sync scripts. Changes appear instantly because Convex provides real-time updates.
```
content/blog/*.md --+
+--> npm run sync --> Convex DB --> Site
content/pages/*.md --+
```
## Sync commands
| Command | Environment | What it syncs |
|---------|-------------|---------------|
| `npm run sync` | Development | Posts + pages to dev Convex |
| `npm run sync:prod` | Production | Posts + pages to prod Convex |
| `npm run sync:discovery` | Development | AGENTS.md + llms.txt |
| `npm run sync:discovery:prod` | Production | AGENTS.md + llms.txt |
| `npm run sync:all` | Development | Everything |
| `npm run sync:all:prod` | Production | Everything |
## How sync works
### Content sync (sync-posts.ts)
1. Reads all `.md` files from `content/blog/` and `content/pages/`
2. Parses frontmatter with `gray-matter`
3. Validates required fields (title, slug, etc.)
4. Calculates reading time if not provided
5. Calls Convex mutations to upsert content
6. Generates raw markdown files in `public/raw/`
### Discovery sync (sync-discovery-files.ts)
1. Reads site configuration from `src/config/siteConfig.ts`
2. Queries Convex for post/page counts
3. Updates `AGENTS.md` with current status
4. Generates `public/llms.txt` with API info
## File locations
| Script | Purpose |
|--------|---------|
| `scripts/sync-posts.ts` | Syncs markdown content |
| `scripts/sync-discovery-files.ts` | Updates discovery files |
| `scripts/import-url.ts` | Imports external URLs |
## Environment variables
| File | Used by |
|------|---------|
| `.env.local` | Development sync (default) |
| `.env.production.local` | Production sync |
Both files contain `VITE_CONVEX_URL` pointing to the Convex deployment.
## What gets synced
### Posts (content/blog/)
- Frontmatter fields (title, description, date, tags, etc.)
- Full markdown content
- Calculated reading time
### Pages (content/pages/)
- Frontmatter fields (title, slug, order, etc.)
- Full markdown content
### Generated files (public/raw/)
For each published post/page, a static markdown file is generated at `public/raw/{slug}.md`. Also generates `public/raw/index.md` listing all content.
## Sync mutations
The sync scripts call these Convex mutations:
```typescript
// Posts
api.posts.syncPostsPublic({ posts: ParsedPost[] })
// Pages
api.pages.syncPagesPublic({ pages: ParsedPage[] })
```
## Adding a new frontmatter field
1. Add to interface in `scripts/sync-posts.ts`
2. Add to Convex schema in `convex/schema.ts`
3. Add to sync mutation in `convex/posts.ts` or `convex/pages.ts`
4. Add to return validators in queries
5. Run `npm run sync` to apply
## Import from URL
```bash
npm run import https://example.com/article
```
Requires `FIRECRAWL_API_KEY`. After import, run sync.
## Troubleshooting
### "VITE_CONVEX_URL not set"
Run `npx convex dev` first to create `.env.local`.
### Posts not appearing
1. Check `published: true` in frontmatter
2. Verify required fields are present
3. Check Convex dashboard for errors
4. Run `npm run sync` again
### Sync to wrong environment
- `npm run sync` = development
- `npm run sync:prod` = production