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

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