mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +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:
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