mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 12:19:18 +00:00
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:
96
.opencode/agent/content-writer.md
Normal file
96
.opencode/agent/content-writer.md
Normal 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
|
||||
72
.opencode/agent/orchestrator.md
Normal file
72
.opencode/agent/orchestrator.md
Normal 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
|
||||
105
.opencode/agent/sync-manager.md
Normal file
105
.opencode/agent/sync-manager.md
Normal 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
|
||||
70
.opencode/command/create-page.md
Normal file
70
.opencode/command/create-page.md
Normal 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
|
||||
```
|
||||
60
.opencode/command/create-post.md
Normal file
60
.opencode/command/create-post.md
Normal 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
|
||||
70
.opencode/command/deploy.md
Normal file
70
.opencode/command/deploy.md
Normal 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
|
||||
62
.opencode/command/import.md
Normal file
62
.opencode/command/import.md
Normal 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.
|
||||
44
.opencode/command/sync-prod.md
Normal file
44
.opencode/command/sync-prod.md
Normal 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
42
.opencode/command/sync.md
Normal 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
6
.opencode/config.json
Normal 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"
|
||||
}
|
||||
47
.opencode/plugin/sync-helper.ts
Normal file
47
.opencode/plugin/sync-helper.ts
Normal 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
187
.opencode/skill/content.md
Normal 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
|
||||

|
||||
```
|
||||
|
||||
### External images
|
||||
|
||||
Use full URLs:
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
### 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
192
.opencode/skill/convex.md
Normal 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
|
||||
194
.opencode/skill/frontmatter.md
Normal file
194
.opencode/skill/frontmatter.md
Normal 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
126
.opencode/skill/sync.md
Normal 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
|
||||
Reference in New Issue
Block a user