diff --git a/FORK_CONFIG.md b/FORK_CONFIG.md index c282a53..151a72a 100644 --- a/FORK_CONFIG.md +++ b/FORK_CONFIG.md @@ -82,21 +82,21 @@ Edit each file individually following the guide below. ### Files to Update -| File | What to Update | -| ----------------------------------- | ------------------------------------------------------------ | +| File | What to Update | +| ----------------------------------- | --------------------------------------------------------------------------- | | `src/config/siteConfig.ts` | Site name, bio, GitHub username, gitHubRepo config, default theme, features | -| `src/pages/Home.tsx` | Intro paragraph, footer links | -| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME` constants | -| `src/pages/DocsPage.tsx` | `SITE_URL` constant | -| `convex/http.ts` | `SITE_URL`, `SITE_NAME` constants | -| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` | -| `netlify/edge-functions/mcp.ts` | `SITE_URL`, `SITE_NAME`, `MCP_SERVER_NAME` constants | -| `scripts/send-newsletter.ts` | Default `SITE_URL` constant | -| `index.html` | Meta tags, JSON-LD, page title | -| `public/llms.txt` | Site info, GitHub link | -| `public/robots.txt` | Sitemap URL | -| `public/openapi.yaml` | Server URL, site name, example URLs | -| `public/.well-known/ai-plugin.json` | Plugin metadata | +| `src/pages/Home.tsx` | Intro paragraph, footer links | +| `src/pages/Post.tsx` | `SITE_URL`, `SITE_NAME` constants | +| `src/pages/DocsPage.tsx` | `SITE_URL` constant | +| `convex/http.ts` | `SITE_URL`, `SITE_NAME` constants | +| `convex/rss.ts` | `SITE_URL`, `SITE_TITLE`, `SITE_DESCRIPTION` | +| `netlify/edge-functions/mcp.ts` | `SITE_URL`, `SITE_NAME`, `MCP_SERVER_NAME` constants | +| `scripts/send-newsletter.ts` | Default `SITE_URL` constant | +| `index.html` | Meta tags, JSON-LD, page title | +| `public/llms.txt` | Site info, GitHub link | +| `public/robots.txt` | Sitemap URL | +| `public/openapi.yaml` | Server URL, site name, example URLs | +| `public/.well-known/ai-plugin.json` | Plugin metadata | --- @@ -750,6 +750,7 @@ The dashboard includes a sync server feature that allows executing sync commands **Setup:** 1. Start the sync server locally: + ```bash npm run sync-server ``` @@ -1141,13 +1142,29 @@ Configure the AI writing assistant. The Dashboard AI Agent supports multiple pro "enableImageGeneration": true, "defaultTextModel": "claude-sonnet-4-20250514", "textModels": [ - { "id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", "provider": "anthropic" }, + { + "id": "claude-sonnet-4-20250514", + "name": "Claude Sonnet 4", + "provider": "anthropic" + }, { "id": "gpt-4o", "name": "GPT-4o", "provider": "openai" }, - { "id": "gemini-2.0-flash", "name": "Gemini 2.0 Flash", "provider": "google" } + { + "id": "gemini-2.0-flash", + "name": "Gemini 2.0 Flash", + "provider": "google" + } ], "imageModels": [ - { "id": "gemini-2.0-flash-exp-image-generation", "name": "Nano Banana", "provider": "google" }, - { "id": "imagen-3.0-generate-002", "name": "Nano Banana Pro", "provider": "google" } + { + "id": "gemini-2.0-flash-exp-image-generation", + "name": "Nano Banana", + "provider": "google" + }, + { + "id": "imagen-3.0-generate-002", + "name": "Nano Banana Pro", + "provider": "google" + } ] } } @@ -1179,11 +1196,11 @@ aiDashboard: { **Environment Variables (Convex):** -| Variable | Provider | Features | -| --- | --- | --- | -| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | -| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | -| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | +| Variable | Provider | Features | +| ------------------- | --------- | ---------------------------------------- | +| `ANTHROPIC_API_KEY` | Anthropic | Claude Sonnet 4 chat | +| `OPENAI_API_KEY` | OpenAI | GPT-4o chat | +| `GOOGLE_AI_API_KEY` | Google | Gemini 2.0 Flash chat + image generation | **Optional system prompt variables:** @@ -1347,15 +1364,15 @@ The script reads from `siteConfig.ts` and queries Convex for live content statis Replace example content in: -| File | Purpose | -| ------------------------------ | -------------------------- | -| `content/blog/*.md` | Blog posts | -| `content/pages/*.md` | Static pages (About, etc.) | -| `content/pages/home.md` | Homepage intro content (slug: `home-intro`, uses blog heading styles) | +| File | Purpose | +| ------------------------------ | -------------------------------------------------------------------------------------------- | +| `content/blog/*.md` | Blog posts | +| `content/pages/*.md` | Static pages (About, etc.) | +| `content/pages/home.md` | Homepage intro content (slug: `home-intro`, uses blog heading styles) | | `content/pages/footer.md` | Footer content (slug: `footer`, syncs via markdown, falls back to siteConfig.defaultContent) | -| `public/images/logo.svg` | Site logo | -| `public/images/og-default.svg` | Default social share image | -| `public/images/logos/*.svg` | Logo gallery images | +| `public/images/logo.svg` | Site logo | +| `public/images/og-default.svg` | Default social share image | +| `public/images/logos/*.svg` | Logo gallery images | --- @@ -1367,12 +1384,12 @@ The site serves pre-rendered HTML with correct canonical URLs and meta tags to s The edge function detects different types of bots and serves appropriate responses: -| Bot Type | Response | Examples | -| ------------------- | ------------------------------------- | ------------------------------------ | -| Social preview bots | Pre-rendered HTML with OG tags | Twitter, Facebook, LinkedIn, Discord | -| Search engine bots | Pre-rendered HTML with correct canonical | Google, Bing, DuckDuckGo | -| AI crawlers | Normal SPA (can render JavaScript) | GPTBot, ClaudeBot, PerplexityBot | -| Regular browsers | Normal SPA | Chrome, Firefox, Safari | +| Bot Type | Response | Examples | +| ------------------- | ---------------------------------------- | ------------------------------------ | +| Social preview bots | Pre-rendered HTML with OG tags | Twitter, Facebook, LinkedIn, Discord | +| Search engine bots | Pre-rendered HTML with correct canonical | Google, Bing, DuckDuckGo | +| AI crawlers | Normal SPA (can render JavaScript) | GPTBot, ClaudeBot, PerplexityBot | +| Regular browsers | Normal SPA | Chrome, Firefox, Safari | ### Customizing Bot Lists @@ -1417,3 +1434,54 @@ curl https://yoursite.com/your-post | grep canonical ### Why This Matters Single-page apps (SPAs) update meta tags via JavaScript after the page loads. Search engines that check raw HTML before rendering may see incorrect canonical URLs. By serving pre-rendered HTML to search engine bots, we ensure they see the correct canonical URL for each page. + +--- + +## Version Control Configuration + +The dashboard includes a built-in Sync version control system. Unlike most features, version control is configured via the Dashboard UI, not `siteConfig.ts` or `fork-config.json`. + +### How to enable + +1. Navigate to `/dashboard` +2. Go to the **Config** section +3. Find the **Version Control** card +4. Toggle **Enable version control** on + +### Features + +- **3-day version history** for all posts, pages, home content, and footer +- **Diff visualization** using unified diff format +- **One-click restore** with automatic backup of current content +- **Automatic cleanup** of versions older than 3 days (runs daily at 3 AM UTC) + +### When versions are captured + +| Source | When created | +| --------- | --------------------------------------------- | +| sync | Before markdown sync updates (`npm run sync`) | +| dashboard | Before saving edits in Dashboard | +| restore | Before restoring a previous version | + +### Viewing version history + +1. Open any post or page in the Dashboard editor +2. Click the **History** button (clock icon) in the editor toolbar +3. Select a version from the list +4. View diff or preview +5. Click **Restore This Version** to revert + +### Technical details + +- Versions stored in `contentVersions` table in Convex +- Settings stored in `versionControlSettings` table +- Cleanup via cron job in `convex/crons.ts` +- Version capture is async (non-blocking via `ctx.scheduler.runAfter`) + +### Why database-based? + +Version control settings are stored in the Convex database rather than config files because: + +1. **Toggle requires real-time state** - UI needs to reflect current setting immediately +2. **Shared across environments** - Same setting for all users of the dashboard +3. **No redeploy needed** - Toggle works instantly without rebuilding diff --git a/TASK.md b/TASK.md index ff5d170..5a8b52e 100644 --- a/TASK.md +++ b/TASK.md @@ -4,10 +4,23 @@ ## Current Status -v2.15.3 ready. Fixed footer not displaying on docs landing page. +v2.16.0 ready. Added version control system for posts and pages. ## Completed +- [x] Version control system (v2.16.0) + - [x] Added contentVersions and versionControlSettings tables to schema + - [x] Created convex/versions.ts with 7 functions (isEnabled, setEnabled, createVersion, getVersionHistory, getVersion, restoreVersion, cleanupOldVersions, getStats) + - [x] Modified cms.ts to capture versions before dashboard edits + - [x] Modified posts.ts to capture versions before sync updates + - [x] Modified pages.ts to capture versions before sync updates + - [x] Added cleanup cron job (daily at 3 AM UTC) for 3-day retention + - [x] Created VersionHistoryModal component with diff view and restore functionality + - [x] Added Version Control card in Dashboard Config section with toggle and stats + - [x] Added History button in Dashboard editor for viewing version history + - [x] Added ~370 lines of CSS for version modal UI + - [x] Updated documentation: docs-dashboard.md, FORK_CONFIG.md, files.md, changelog.md, task.md, changelog-page.md + - [x] Footer not displaying on /docs landing page fix (v2.15.3) - [x] DocsPage.tsx was missing Footer component entirely - [x] Added Footer import and footerPage query to DocsPage.tsx diff --git a/changelog.md b/changelog.md index fc0f48d..8112a47 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [2.16.0] - 2026-01-09 + +### Added + +- Sync version control system + - 3-day version history for posts, pages, home content, and footer + - Dashboard toggle to enable/disable version control + - Version history modal with unified diff visualization using DiffCodeBlock component + - Preview mode to view previous version content + - One-click restore with automatic backup of current state + - Automatic cleanup of versions older than 3 days (daily cron at 3 AM UTC) + - Version stats display in Config section (total, posts, pages) + +### Technical + +- New `convex/versions.ts` with 7 functions: + - `isEnabled` / `setEnabled` - Toggle version control + - `createVersion` - Capture content snapshot (internal mutation) + - `getVersionHistory` / `getVersion` - Query version data + - `restoreVersion` - Restore with backup creation + - `cleanupOldVersions` - Batch delete old versions + - `getStats` - Version count statistics +- New `contentVersions` table in schema with indexes: + - `by_content` - Query by content type and ID + - `by_slug` - Query by content type and slug + - `by_createdAt` - For cleanup queries + - `by_content_createdAt` - Compound index for history +- New `versionControlSettings` table for toggle state +- New `src/components/VersionHistoryModal.tsx` component +- Updated `convex/cms.ts` to capture versions before dashboard edits +- Updated `convex/posts.ts` to capture versions before sync updates +- Updated `convex/pages.ts` to capture versions before sync updates +- Updated `convex/crons.ts` with daily cleanup job +- Added ~370 lines of CSS for version modal UI + ## [2.15.3] - 2026-01-09 ### Fixed @@ -133,7 +168,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Unified and split (side-by-side) view modes with toggle button - Theme-aware colors (dark/light/tan/cloud support) - Copy button for diff content - - Automatic routing: ```diff and ```patch blocks use enhanced renderer + - Automatic routing: `diff and `patch blocks use enhanced renderer - New blog post: "How to Use Code Blocks" with syntax highlighting and diff examples - DiffCodeBlock component (`src/components/DiffCodeBlock.tsx`) @@ -624,7 +659,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Technical -- Updated: `scripts/configure-fork.ts` - Added ES module compatible __dirname using fileURLToPath +- Updated: `scripts/configure-fork.ts` - Added ES module compatible \_\_dirname using fileURLToPath ## [2.2.0] - 2025-12-30 diff --git a/content/blog/how-to-use-agentmail.md b/content/blog/how-to-use-agentmail.md index f9a89a9..c817d98 100644 --- a/content/blog/how-to-use-agentmail.md +++ b/content/blog/how-to-use-agentmail.md @@ -7,7 +7,7 @@ published: true featured: true featuredOrder: 5 layout: "sidebar" -blogFeatured: true +blogFeatured: false image: /images/agentmail-blog.png tags: ["agentmail", "newsletter", "email", "setup"] docsSection: true diff --git a/content/blog/how-to-use-mcp-server.md b/content/blog/how-to-use-mcp-server.md index 68e3efe..5ae0644 100644 --- a/content/blog/how-to-use-mcp-server.md +++ b/content/blog/how-to-use-mcp-server.md @@ -5,7 +5,7 @@ date: "2025-12-28" slug: "how-to-use-mcp-server" image: /images/mcp-blog.png published: true -blogFeatured: true +blogFeatured: false layout: "sidebar" tags: ["mcp", "cursor", "ai", "tutorial", "netlify"] docsSection: true diff --git a/content/blog/how-to-use-the-markdown-sync-dashboard.md b/content/blog/how-to-use-the-markdown-sync-dashboard.md index 9877174..f7f7bb5 100644 --- a/content/blog/how-to-use-the-markdown-sync-dashboard.md +++ b/content/blog/how-to-use-the-markdown-sync-dashboard.md @@ -9,6 +9,7 @@ readTime: "8 min read" featured: true layout: "sidebar" featuredOrder: 2 +blogFeatured: true image: /images/dashboard.png excerpt: "A complete guide to using the dashboard for managing your markdown blog without leaving your browser." docsSection: true diff --git a/content/pages/changelog-page.md b/content/pages/changelog-page.md index 136c847..f1e9396 100644 --- a/content/pages/changelog-page.md +++ b/content/pages/changelog-page.md @@ -11,6 +11,56 @@ docsSectionOrder: 4 All notable changes to this project. +## v2.16.0 + +Released January 9, 2026 + +**Version control system** + +Added a Sync version control system for tracking changes to posts, pages, home content, and footer. + +**Features:** + +- 3-day version history for all content +- Dashboard toggle to enable/disable version control +- Version history modal with unified diff visualization +- Preview mode to view previous version content +- One-click restore with automatic backup of current state +- Automatic cleanup of versions older than 3 days (daily cron at 3 AM UTC) +- Version stats display in Config section + +**How to use:** + +1. Navigate to Dashboard > Config +2. Find the "Version Control" card +3. Toggle "Enable version control" on +4. Edit posts/pages or run sync commands to capture versions +5. Click the History button in the editor to view version history +6. Select a version to view diff or preview, then click "Restore This Version" + +**Technical:** + +- New `convex/versions.ts` with 7 functions (isEnabled, setEnabled, createVersion, getVersionHistory, getVersion, restoreVersion, cleanupOldVersions, getStats) +- New `contentVersions` table with indexes for efficient queries +- New `versionControlSettings` table for toggle state +- New `VersionHistoryModal.tsx` component using existing DiffCodeBlock +- Version capture integrated into cms.ts, posts.ts, and pages.ts +- Cleanup cron job in crons.ts + +**Files changed:** + +- `convex/schema.ts` - Added contentVersions and versionControlSettings tables +- `convex/versions.ts` - New file with all version control logic +- `convex/cms.ts` - Added version capture before dashboard edits +- `convex/posts.ts` - Added version capture before sync updates +- `convex/pages.ts` - Added version capture before sync updates +- `convex/crons.ts` - Added daily cleanup job +- `src/components/VersionHistoryModal.tsx` - New version history modal +- `src/pages/Dashboard.tsx` - Added Version Control config card and History button +- `src/styles/global.css` - Added ~370 lines of version modal CSS + +--- + ## v2.15.3 Released January 9, 2026 @@ -208,7 +258,7 @@ Diff and patch code blocks now render with enhanced visualization powered by @pi - View toggle button to switch between modes - Theme-aware colors matching dark/light/tan/cloud themes - Copy button for copying raw diff content -- Automatic routing: Use ```diff or ```patch in markdown +- Automatic routing: Use `diff or `patch in markdown **New documentation:** @@ -351,6 +401,7 @@ semanticSearch: { ``` When disabled: + - Search modal shows only keyword search (no mode toggle) - Embedding generation skipped during sync (saves API costs) - Existing embeddings preserved in database (no data loss) @@ -379,12 +430,12 @@ Search now supports two modes accessible via Cmd+K: **When to use each mode:** -| Use Case | Mode | -|----------|------| -| Specific code, commands, exact phrases | Keyword | +| Use Case | Mode | +| ----------------------------------------- | -------- | +| Specific code, commands, exact phrases | Keyword | | Conceptual questions ("how do I deploy?") | Semantic | -| Need to highlight matches on page | Keyword | -| Not sure of exact terminology | Semantic | +| Need to highlight matches on page | Keyword | +| Not sure of exact terminology | Semantic | **Configuration:** diff --git a/content/pages/docs-dashboard.md b/content/pages/docs-dashboard.md index d258d47..b45ca7f 100644 --- a/content/pages/docs-dashboard.md +++ b/content/pages/docs-dashboard.md @@ -2,7 +2,7 @@ title: "Dashboard" slug: "docs-dashboard" published: true -order: 5 +order: 1 showInNav: false layout: "sidebar" rightSidebar: true @@ -375,6 +375,63 @@ This creates a file in `content/blog/` that requires syncing. **Note:** The dashboard provides a UI for these commands. When the sync server is running (`npm run sync-server`), you can execute commands directly from the dashboard with real-time output. Otherwise, the dashboard shows commands in a modal for copying to your terminal. +### Version control + +The dashboard includes a Sync version control system that tracks changes to posts, pages, home content, and footer content. + +**Features:** + +- 3-day version history for all content +- Toggle to enable/disable version control +- View version history with unified diff visualization +- Preview previous versions +- One-click restore with automatic backup +- Automatic cleanup of versions older than 3 days + +**Enabling version control:** + +1. Navigate to Dashboard > Config +2. Find the "Version Control" card +3. Toggle "Enable version control" on + +When enabled, versions are captured: + +- Before sync updates (from markdown files) +- Before dashboard edits (Save Changes button) +- Before restoring a previous version + +**Viewing version history:** + +1. Open any post or page in the Dashboard editor +2. Click the clock (History) button in the editor toolbar +3. Select a version from the list to view details +4. Toggle between "Diff" and "Preview" modes +5. Click "Restore This Version" to revert + +**How it works:** + +- Versions are stored in the `contentVersions` table +- Settings stored in `versionControlSettings` table (database, not config file) +- Cleanup runs daily at 3:00 AM UTC via cron job +- Restore creates a backup of current content before reverting +- Uses existing DiffCodeBlock component for diff visualization + +**Version sources:** + +| Source | When created | +| --------- | ---------------------------- | +| sync | Before markdown sync updates | +| dashboard | Before dashboard edits | +| restore | Before restoring a version | + +**Stats display:** + +The Version Control card in Config shows: + +- Total version count +- Post versions count +- Page versions count + ### Environment variables **Convex Environment Variables:** @@ -394,7 +451,7 @@ npx convex env set VARIABLE_NAME value **Local Environment Variables (.env.local):** -| Variable | Description | -| ------------------- | ------------------------------------------ | -| `VITE_CONVEX_URL` | Your Convex deployment URL (auto-created) | -| `FIRECRAWL_API_KEY` | For CLI import command only | +| Variable | Description | +| ------------------- | ----------------------------------------- | +| `VITE_CONVEX_URL` | Your Convex deployment URL (auto-created) | +| `FIRECRAWL_API_KEY` | For CLI import command only | diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 5b408d5..5aa5f93 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -29,6 +29,7 @@ import type * as search from "../search.js"; import type * as semanticSearch from "../semanticSearch.js"; import type * as semanticSearchQueries from "../semanticSearchQueries.js"; import type * as stats from "../stats.js"; +import type * as versions from "../versions.js"; import type { ApiFromModules, @@ -58,6 +59,7 @@ declare const fullApi: ApiFromModules<{ semanticSearch: typeof semanticSearch; semanticSearchQueries: typeof semanticSearchQueries; stats: typeof stats; + versions: typeof versions; }>; /** diff --git a/convex/cms.ts b/convex/cms.ts index 70f8ef0..82985e9 100644 --- a/convex/cms.ts +++ b/convex/cms.ts @@ -1,5 +1,6 @@ import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; +import { internal } from "./_generated/api"; // Shared validator for post data const postDataValidator = v.object({ @@ -150,6 +151,17 @@ export const updatePost = mutation({ } } + // Capture version before update (async, non-blocking) + await ctx.scheduler.runAfter(0, internal.versions.createVersion, { + contentType: "post", + contentId: args.id, + slug: existing.slug, + title: existing.title, + content: existing.content, + description: existing.description, + source: "dashboard", + }); + await ctx.db.patch(args.id, { ...args.post, lastSyncedAt: Date.now(), @@ -253,6 +265,16 @@ export const updatePage = mutation({ } } + // Capture version before update (async, non-blocking) + await ctx.scheduler.runAfter(0, internal.versions.createVersion, { + contentType: "page", + contentId: args.id, + slug: existing.slug, + title: existing.title, + content: existing.content, + source: "dashboard", + }); + await ctx.db.patch(args.id, { ...args.page, lastSyncedAt: Date.now(), diff --git a/convex/crons.ts b/convex/crons.ts index 14de24d..92e8e0c 100644 --- a/convex/crons.ts +++ b/convex/crons.ts @@ -35,5 +35,14 @@ crons.cron( } ); +// Clean up old content versions daily at 3:00 AM UTC +// Deletes versions older than 3 days to maintain storage efficiency +crons.cron( + "cleanup old content versions", + "0 3 * * *", // 3:00 AM UTC daily + internal.versions.cleanupOldVersions, + {} +); + export default crons; diff --git a/convex/pages.ts b/convex/pages.ts index 7d66583..b7e1ef3 100644 --- a/convex/pages.ts +++ b/convex/pages.ts @@ -1,5 +1,6 @@ import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; +import { internal } from "./_generated/api"; // Get all pages (published and unpublished) for dashboard admin view export const listAll = query({ @@ -392,6 +393,15 @@ export const syncPagesPublic = mutation({ skipped++; continue; } + // Capture version before update (async, non-blocking) + await ctx.scheduler.runAfter(0, internal.versions.createVersion, { + contentType: "page", + contentId: existing._id, + slug: existing.slug, + title: existing.title, + content: existing.content, + source: "sync", + }); // Update existing sync page await ctx.db.patch(existing._id, { title: page.title, diff --git a/convex/posts.ts b/convex/posts.ts index ea6e427..f82c1cd 100644 --- a/convex/posts.ts +++ b/convex/posts.ts @@ -1,5 +1,6 @@ import { query, mutation, internalMutation, internalQuery } from "./_generated/server"; import { v } from "convex/values"; +import { internal } from "./_generated/api"; // Get all posts (published and unpublished) for dashboard admin view export const listAll = query({ @@ -545,6 +546,16 @@ export const syncPostsPublic = mutation({ skipped++; continue; } + // Capture version before update (async, non-blocking) + await ctx.scheduler.runAfter(0, internal.versions.createVersion, { + contentType: "post", + contentId: existing._id, + slug: existing.slug, + title: existing.title, + content: existing.content, + description: existing.description, + source: "sync", + }); // Update existing sync post await ctx.db.patch(existing._id, { title: post.title, diff --git a/convex/schema.ts b/convex/schema.ts index 1c95585..a1baeb3 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -244,4 +244,32 @@ export default defineSchema({ ) ), // Optional sources cited in the response }).index("by_stream", ["streamId"]), + + // Content version history for posts and pages + // Stores snapshots before each update for 3-day retention + contentVersions: defineTable({ + contentType: v.union(v.literal("post"), v.literal("page")), // Type of content + contentId: v.string(), // ID of the post or page (stored as string for flexibility) + slug: v.string(), // Slug for display and querying + title: v.string(), // Title at time of snapshot + content: v.string(), // Full markdown content at time of snapshot + description: v.optional(v.string()), // Description (posts only) + createdAt: v.number(), // Timestamp when version was created + source: v.union( + v.literal("sync"), + v.literal("dashboard"), + v.literal("restore") + ), // What triggered the version capture + }) + .index("by_content", ["contentType", "contentId"]) + .index("by_slug", ["contentType", "slug"]) + .index("by_createdAt", ["createdAt"]) + .index("by_content_createdAt", ["contentType", "contentId", "createdAt"]), + + // Version control settings + // Stores toggle state for version control feature + versionControlSettings: defineTable({ + key: v.string(), // Setting key: "enabled" + value: v.boolean(), // Setting value + }).index("by_key", ["key"]), }); diff --git a/convex/versions.ts b/convex/versions.ts new file mode 100644 index 0000000..75d2616 --- /dev/null +++ b/convex/versions.ts @@ -0,0 +1,276 @@ +import { query, mutation, internalMutation } from "./_generated/server"; +import { v } from "convex/values"; +import { Id } from "./_generated/dataModel"; + +// Retention period: 3 days in milliseconds +const RETENTION_MS = 3 * 24 * 60 * 60 * 1000; + +// Check if version control is enabled +export const isEnabled = query({ + args: {}, + returns: v.boolean(), + handler: async (ctx) => { + const setting = await ctx.db + .query("versionControlSettings") + .withIndex("by_key", (q) => q.eq("key", "enabled")) + .first(); + return setting?.value === true; + }, +}); + +// Toggle version control on/off +export const setEnabled = mutation({ + args: { enabled: v.boolean() }, + returns: v.null(), + handler: async (ctx, args) => { + const existing = await ctx.db + .query("versionControlSettings") + .withIndex("by_key", (q) => q.eq("key", "enabled")) + .first(); + + if (existing) { + await ctx.db.patch(existing._id, { value: args.enabled }); + } else { + await ctx.db.insert("versionControlSettings", { + key: "enabled", + value: args.enabled, + }); + } + return null; + }, +}); + +// Create a version snapshot (called before updates) +// This is an internal mutation to be called from other mutations +export const createVersion = internalMutation({ + args: { + contentType: v.union(v.literal("post"), v.literal("page")), + contentId: v.string(), + slug: v.string(), + title: v.string(), + content: v.string(), + description: v.optional(v.string()), + source: v.union( + v.literal("sync"), + v.literal("dashboard"), + v.literal("restore") + ), + }, + returns: v.union(v.id("contentVersions"), v.null()), + handler: async (ctx, args) => { + // Check if version control is enabled + const setting = await ctx.db + .query("versionControlSettings") + .withIndex("by_key", (q) => q.eq("key", "enabled")) + .first(); + + if (setting?.value !== true) { + return null; + } + + // Create version snapshot + const versionId = await ctx.db.insert("contentVersions", { + contentType: args.contentType, + contentId: args.contentId, + slug: args.slug, + title: args.title, + content: args.content, + description: args.description, + createdAt: Date.now(), + source: args.source, + }); + + return versionId; + }, +}); + +// Get version history for a piece of content +export const getVersionHistory = query({ + args: { + contentType: v.union(v.literal("post"), v.literal("page")), + contentId: v.string(), + }, + returns: v.array( + v.object({ + _id: v.id("contentVersions"), + title: v.string(), + createdAt: v.number(), + source: v.union( + v.literal("sync"), + v.literal("dashboard"), + v.literal("restore") + ), + contentPreview: v.string(), + }) + ), + handler: async (ctx, args) => { + const versions = await ctx.db + .query("contentVersions") + .withIndex("by_content", (q) => + q.eq("contentType", args.contentType).eq("contentId", args.contentId) + ) + .order("desc") + .collect(); + + return versions.map((v) => ({ + _id: v._id, + title: v.title, + createdAt: v.createdAt, + source: v.source, + contentPreview: + v.content.slice(0, 150) + (v.content.length > 150 ? "..." : ""), + })); + }, +}); + +// Get a specific version's full content +export const getVersion = query({ + args: { versionId: v.id("contentVersions") }, + returns: v.union( + v.object({ + _id: v.id("contentVersions"), + contentType: v.union(v.literal("post"), v.literal("page")), + contentId: v.string(), + slug: v.string(), + title: v.string(), + content: v.string(), + description: v.optional(v.string()), + createdAt: v.number(), + source: v.union( + v.literal("sync"), + v.literal("dashboard"), + v.literal("restore") + ), + }), + v.null() + ), + handler: async (ctx, args) => { + const version = await ctx.db.get(args.versionId); + if (!version) return null; + + return { + _id: version._id, + contentType: version.contentType, + contentId: version.contentId, + slug: version.slug, + title: version.title, + content: version.content, + description: version.description, + createdAt: version.createdAt, + source: version.source, + }; + }, +}); + +// Restore a previous version +export const restoreVersion = mutation({ + args: { versionId: v.id("contentVersions") }, + returns: v.object({ + success: v.boolean(), + message: v.string(), + }), + handler: async (ctx, args) => { + const version = await ctx.db.get(args.versionId); + if (!version) { + return { success: false, message: "Version not found" }; + } + + // Get current content to create a backup before restoring + let currentContent; + if (version.contentType === "post") { + currentContent = await ctx.db.get( + version.contentId as Id<"posts"> + ); + } else { + currentContent = await ctx.db.get( + version.contentId as Id<"pages"> + ); + } + + if (!currentContent) { + return { success: false, message: "Original content not found" }; + } + + // Create backup version of current state before restoring + await ctx.db.insert("contentVersions", { + contentType: version.contentType, + contentId: version.contentId, + slug: version.slug, + title: currentContent.title, + content: currentContent.content, + description: + "description" in currentContent ? currentContent.description : undefined, + createdAt: Date.now(), + source: "restore", + }); + + // Restore the content + if (version.contentType === "post") { + await ctx.db.patch(version.contentId as Id<"posts">, { + title: version.title, + content: version.content, + description: version.description || "", + lastSyncedAt: Date.now(), + }); + } else { + await ctx.db.patch(version.contentId as Id<"pages">, { + title: version.title, + content: version.content, + lastSyncedAt: Date.now(), + }); + } + + return { success: true, message: "Version restored successfully" }; + }, +}); + +// Clean up versions older than 3 days +// Called by cron job +export const cleanupOldVersions = internalMutation({ + args: {}, + returns: v.number(), + handler: async (ctx) => { + const cutoff = Date.now() - RETENTION_MS; + + // Get old versions using the createdAt index + const oldVersions = await ctx.db + .query("contentVersions") + .withIndex("by_createdAt", (q) => q.lt("createdAt", cutoff)) + .take(1000); + + // Delete the batch + await Promise.all(oldVersions.map((version) => ctx.db.delete(version._id))); + + return oldVersions.length; + }, +}); + +// Get version control stats (for dashboard display) +export const getStats = query({ + args: {}, + returns: v.object({ + enabled: v.boolean(), + totalVersions: v.number(), + oldestVersion: v.union(v.number(), v.null()), + newestVersion: v.union(v.number(), v.null()), + }), + handler: async (ctx) => { + const setting = await ctx.db + .query("versionControlSettings") + .withIndex("by_key", (q) => q.eq("key", "enabled")) + .first(); + + const versions = await ctx.db.query("contentVersions").collect(); + + const timestamps = versions.map((v) => v.createdAt); + const oldest = timestamps.length > 0 ? Math.min(...timestamps) : null; + const newest = timestamps.length > 0 ? Math.max(...timestamps) : null; + + return { + enabled: setting?.value === true, + totalVersions: versions.length, + oldestVersion: oldest, + newestVersion: newest, + }; + }, +}); diff --git a/files.md b/files.md index 8bbf155..2a37e52 100644 --- a/files.md +++ b/files.md @@ -49,7 +49,7 @@ A brief description of each file in the codebase. | `TagPage.tsx` | Tag archive page displaying posts filtered by a specific tag. Includes view mode toggle (list/cards) with localStorage persistence | | `AuthorPage.tsx` | Author archive page displaying posts by a specific author. Includes view mode toggle (list/cards) with localStorage persistence. Author name clickable in posts links to this page. | | `Write.tsx` | Three-column markdown writing page with Cursor docs-style UI, frontmatter reference with copy buttons, theme toggle, font switcher (serif/sans/monospace), localStorage persistence, and optional AI Agent mode (toggleable via siteConfig.aiChat.enabledOnWritePage). When enabled, Agent replaces the textarea with AIChatView component. Includes scroll prevention when switching to Agent mode to prevent page jump. Title changes to "Agent" when in AI chat mode. | -| `Dashboard.tsx` | Centralized dashboard at `/dashboard` for content management and site configuration. **Cloud CMS Features:** Direct database save ("Save to DB" button), source tracking (Dashboard vs Synced badges), delete confirmation modal with warning, CRUD operations for dashboard-created content. **Content Management:** Posts and Pages list views with filtering, search, pagination, items per page selector, source badges, delete buttons (dashboard content only); Post/Page editor with markdown editor, live preview, "Save Changes" button, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown, export to markdown; Write Post/Page sections with three editor modes (Markdown, Rich Text via Quill, Preview), full-screen writing interface. **Rich Text Editor:** Quill-based WYSIWYG editor with toolbar (headers, bold, italic, lists, links, code, blockquote), automatic HTML-to-Markdown conversion on mode switch, theme-aware styling. **AI Agent:** Tab-based UI for Chat and Image Generation, multi-model selector (Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash), image generation with Nano Banana models and aspect ratio selection. **Other Features:** Newsletter management (all Newsletter Admin features integrated); Content import (direct database import via Firecrawl, no file sync needed); Site configuration (Config Generator UI); Index HTML editor; Analytics (real-time stats dashboard); Sync commands UI with sync server integration; Header sync buttons; Dashboard search; Toast notifications; Command modal; Mobile responsive design. Uses Convex queries for real-time data, localStorage for preferences, ReactMarkdown for preview. Optional WorkOS authentication via siteConfig.dashboard.requireAuth. | +| `Dashboard.tsx` | Centralized dashboard at `/dashboard` for content management and site configuration. **Cloud CMS Features:** Direct database save ("Save to DB" button), source tracking (Dashboard vs Synced badges), delete confirmation modal with warning, CRUD operations for dashboard-created content. **Content Management:** Posts and Pages list views with filtering, search, pagination, items per page selector, source badges, delete buttons (dashboard content only); Post/Page editor with markdown editor, live preview, "Save Changes" button, draggable/resizable frontmatter sidebar (200px-600px), independent scrolling, download markdown, export to markdown; Write Post/Page sections with three editor modes (Markdown, Rich Text via Quill, Preview), full-screen writing interface. **Rich Text Editor:** Quill-based WYSIWYG editor with toolbar (headers, bold, italic, lists, links, code, blockquote), automatic HTML-to-Markdown conversion on mode switch, theme-aware styling. **AI Agent:** Tab-based UI for Chat and Image Generation, multi-model selector (Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash), image generation with Nano Banana models and aspect ratio selection. **Other Features:** Newsletter management (all Newsletter Admin features integrated); Content import (direct database import via Firecrawl, no file sync needed); Site configuration (Config Generator UI with Version Control toggle); Index HTML editor; Analytics (real-time stats dashboard); Sync commands UI with sync server integration; Header sync buttons; Dashboard search; Toast notifications; Command modal; Version history modal for viewing diffs and restoring previous versions; Mobile responsive design. Uses Convex queries for real-time data, localStorage for preferences, ReactMarkdown for preview. Optional WorkOS authentication via siteConfig.dashboard.requireAuth. | | `Callback.tsx` | OAuth callback handler for WorkOS authentication. Handles redirect from WorkOS after user login, exchanges authorization code for user information, then redirects to dashboard. Only used when WorkOS is configured. | | `NewsletterAdmin.tsx` | Three-column newsletter admin page for managing subscribers and sending newsletters. Left sidebar with navigation and stats, main area with searchable subscriber list, right sidebar with send newsletter panel and recent sends. Access at /newsletter-admin, configurable via siteConfig.newsletterAdmin. | @@ -79,6 +79,7 @@ A brief description of each file in the codebase. | `ContactForm.tsx` | Contact form component with name, email, and message fields. Displays when contactForm: true in frontmatter. Submits to Convex which sends email via AgentMail to configured recipient. Requires AGENTMAIL_API_KEY and AGENTMAIL_INBOX environment variables. Includes honeypot field for bot protection. | | `SocialFooter.tsx` | Social footer component with social icons on left (GitHub, Twitter/X, LinkedIn, Instagram, YouTube, TikTok, Discord, Website) and copyright on right. Configurable via siteConfig.socialFooter. Shows below main footer on homepage, blog posts, and pages. Supports frontmatter override via showSocialFooter: true/false. Auto-updates copyright year. Exports `platformIcons` for reuse in header. | | `AskAIModal.tsx` | Ask AI chat modal for RAG-based Q&A about site content. Opens via header button (Cmd+J) when enabled. Uses Convex Persistent Text Streaming for real-time responses. Supports model selection (Claude, GPT-4o). Features streaming messages with markdown rendering, internal link handling via React Router, and source citations. Requires siteConfig.askAI.enabled and siteConfig.semanticSearch.enabled. | +| `VersionHistoryModal.tsx` | Version history modal for viewing and restoring previous content versions. Shows version list with dates and source badges, diff view using DiffCodeBlock component, preview mode, and one-click restore. Used in Dashboard editor when version control is enabled. | ### Context (`src/context/`) @@ -112,8 +113,8 @@ A brief description of each file in the codebase. | File | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------ | -| `schema.ts` | Database schema (posts, pages, viewCounts, pageViews, activeSessions, aiChats, aiGeneratedImages, newsletterSubscribers, newsletterSentPosts, contactMessages, askAISessions) with indexes for tag queries (by_tags), AI queries, blog featured posts (by_blogFeatured), source tracking (by_source), and vector search (by_embedding). Posts and pages include showSocialFooter, showImageAtTop, blogFeatured, contactForm, source, and embedding fields for frontmatter control, cloud CMS tracking, and semantic search. askAISessions stores question, streamId, model, and sources for Ask AI RAG feature. | -| `cms.ts` | CRUD mutations for dashboard cloud CMS: createPost, updatePost, deletePost, createPage, updatePage, deletePage, exportPostAsMarkdown, exportPageAsMarkdown. Posts/pages created via dashboard have `source: "dashboard"` (protected from sync overwrites). | +| `schema.ts` | Database schema (posts, pages, viewCounts, pageViews, activeSessions, aiChats, aiGeneratedImages, newsletterSubscribers, newsletterSentPosts, contactMessages, askAISessions, contentVersions, versionControlSettings) with indexes for tag queries (by_tags), AI queries, blog featured posts (by_blogFeatured), source tracking (by_source), vector search (by_embedding), and version history (by_content, by_createdAt). Posts and pages include showSocialFooter, showImageAtTop, blogFeatured, contactForm, source, and embedding fields for frontmatter control, cloud CMS tracking, and semantic search. contentVersions stores snapshots before content updates. versionControlSettings stores the enable/disable toggle. | +| `cms.ts` | CRUD mutations for dashboard cloud CMS: createPost, updatePost, deletePost, createPage, updatePage, deletePage, exportPostAsMarkdown, exportPageAsMarkdown. Posts/pages created via dashboard have `source: "dashboard"` (protected from sync overwrites). Captures versions before updates when version control is enabled. | | `importAction.ts` | Server-side Convex action for direct URL import via Firecrawl API. Scrapes URL, converts to markdown, saves directly to database with `source: "dashboard"`. Requires FIRECRAWL_API_KEY environment variable. | | `posts.ts` | Queries and mutations for blog posts, view counts, getAllTags, getPostsByTag, getRelatedPosts, and getBlogFeaturedPosts. Includes tag-based queries for tag pages and related posts functionality. | | `pages.ts` | Queries and mutations for static pages | @@ -123,7 +124,7 @@ A brief description of each file in the codebase. | `embeddings.ts` | Embedding generation actions using OpenAI text-embedding-ada-002 | | `embeddingsQueries.ts` | Internal queries and mutations for embedding storage and retrieval | | `stats.ts` | Real-time stats with aggregate component for O(log n) counts, page view recording, session heartbeat | -| `crons.ts` | Cron jobs for stale session cleanup (every 5 minutes), weekly newsletter digest (Sundays 9am UTC), and weekly stats summary (Mondays 9am UTC). Uses environment variables SITE_URL and SITE_NAME for email content. | +| `crons.ts` | Cron jobs for stale session cleanup (every 5 minutes), weekly newsletter digest (Sundays 9am UTC), weekly stats summary (Mondays 9am UTC), and version cleanup (daily 3am UTC). Uses environment variables SITE_URL and SITE_NAME for email content. | | `http.ts` | HTTP endpoints: sitemap (includes tag pages), API (update SITE_URL/SITE_NAME when forking, uses www.markdown.fast), Open Graph HTML generation for social crawlers with hreflang and twitter:site meta tags | | `rss.ts` | RSS feed generation (update SITE_URL/SITE_TITLE when forking, uses www.markdown.fast) | | `auth.config.ts` | Convex authentication configuration for WorkOS. Defines JWT providers for WorkOS API and user management. Requires WORKOS_CLIENT_ID environment variable in Convex. Optional - only needed if using WorkOS authentication for dashboard. | @@ -133,6 +134,7 @@ A brief description of each file in the codebase. | `newsletter.ts` | Newsletter mutations and queries: subscribe, unsubscribe, getSubscriberCount, getActiveSubscribers, getAllSubscribers (admin), deleteSubscriber (admin), getNewsletterStats, getPostsForNewsletter, wasPostSent, recordPostSent, scheduleSendPostNewsletter, scheduleSendCustomNewsletter, scheduleSendStatsSummary, getStatsForSummary. | | `newsletterActions.ts` | Newsletter actions (Node.js runtime): sendPostNewsletter, sendCustomNewsletter, sendWeeklyDigest, notifyNewSubscriber, sendWeeklyStatsSummary. Uses AgentMail SDK for email delivery. Includes markdown-to-HTML conversion for custom emails. | | `contact.ts` | Contact form mutations and actions: submitContact, sendContactEmail (AgentMail API), markEmailSent. | +| `versions.ts` | Version control system: isEnabled, setEnabled, createVersion, getVersionHistory, getVersion, restoreVersion, cleanupOldVersions, getStats. Captures content snapshots before updates, provides 3-day history with diff view and restore functionality. | | `askAI.ts` | Ask AI session management: createSession mutation (creates streaming session with question/model in DB), getStreamBody query (for database fallback), getSessionByStreamId internal query (retrieves question/model for HTTP action). Uses Persistent Text Streaming component. | | `askAI.node.ts` | Ask AI HTTP action for streaming responses (Node.js runtime). Retrieves question from database, performs vector search using existing semantic search embeddings, generates AI response via Anthropic Claude or OpenAI GPT-4o, streams via appendChunk. Includes CORS headers and source citations. | | `convex.config.ts` | Convex app configuration with aggregate component registrations (pageViewsByPath, totalPageViews, uniqueVisitors) and persistentTextStreaming component | diff --git a/package-lock.json b/package-lock.json index ed589db..bcedd4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "agentmail": "^0.1.15", "convex": "^1.17.4", "date-fns": "^3.3.1", + "diff": "^8.0.2", "gray-matter": "^4.0.3", "lucide-react": "^0.344.0", "openai": "^4.79.0", diff --git a/package.json b/package.json index 4b3f68b..5d6ffc0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "agentmail": "^0.1.15", "convex": "^1.17.4", "date-fns": "^3.3.1", + "diff": "^8.0.2", "gray-matter": "^4.0.3", "lucide-react": "^0.344.0", "openai": "^4.79.0", diff --git a/public/raw/about.md b/public/raw/about.md index fa722a7..15a3314 100644 --- a/public/raw/about.md +++ b/public/raw/about.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. Write markdown, sync from the terminal. Your content is instantly available to browsers, LLMs, and AI agents. Built on Convex and Netlify. diff --git a/public/raw/changelog.md b/public/raw/changelog.md index 21a3c45..46e1a67 100644 --- a/public/raw/changelog.md +++ b/public/raw/changelog.md @@ -2,11 +2,90 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- All notable changes to this project. +## v2.16.0 + +Released January 9, 2026 + +**Version control system** + +Added a Sync version control system for tracking changes to posts, pages, home content, and footer. + +**Features:** + +- 3-day version history for all content +- Dashboard toggle to enable/disable version control +- Version history modal with unified diff visualization +- Preview mode to view previous version content +- One-click restore with automatic backup of current state +- Automatic cleanup of versions older than 3 days (daily cron at 3 AM UTC) +- Version stats display in Config section + +**How to use:** + +1. Navigate to Dashboard > Config +2. Find the "Version Control" card +3. Toggle "Enable version control" on +4. Edit posts/pages or run sync commands to capture versions +5. Click the History button in the editor to view version history +6. Select a version to view diff or preview, then click "Restore This Version" + +**Technical:** + +- New `convex/versions.ts` with 7 functions (isEnabled, setEnabled, createVersion, getVersionHistory, getVersion, restoreVersion, cleanupOldVersions, getStats) +- New `contentVersions` table with indexes for efficient queries +- New `versionControlSettings` table for toggle state +- New `VersionHistoryModal.tsx` component using existing DiffCodeBlock +- Version capture integrated into cms.ts, posts.ts, and pages.ts +- Cleanup cron job in crons.ts + +**Files changed:** + +- `convex/schema.ts` - Added contentVersions and versionControlSettings tables +- `convex/versions.ts` - New file with all version control logic +- `convex/cms.ts` - Added version capture before dashboard edits +- `convex/posts.ts` - Added version capture before sync updates +- `convex/pages.ts` - Added version capture before sync updates +- `convex/crons.ts` - Added daily cleanup job +- `src/components/VersionHistoryModal.tsx` - New version history modal +- `src/pages/Dashboard.tsx` - Added Version Control config card and History button +- `src/styles/global.css` - Added ~370 lines of version modal CSS + +--- + +## v2.15.3 + +Released January 9, 2026 + +**Footer not displaying on /docs landing page fix** + +Fixed an issue where the footer was not displaying on the `/docs` landing page even when `showFooter: true` was set in the frontmatter. The `DocsPage.tsx` component (which handles the `/docs` route with `docsLanding: true`) was missing the Footer component entirely. + +**Fixes:** + +- Added Footer component to DocsPage.tsx +- Footer now respects `showFooter` frontmatter field on docs landing pages +- Added AI chat support to docs landing page via `aiChatEnabled` and `pageContent` props + +**Technical:** + +- Added `Footer` import and `footerPage` query to fetch footer content +- Added footer rendering logic after BlogPost component (same pattern as Post.tsx) +- Updated `getDocsLandingPage` query in `convex/pages.ts` to return `showFooter`, `footer`, `excerpt`, and `aiChat` fields +- Updated `getDocsLandingPost` query in `convex/posts.ts` to return `showFooter`, `footer`, and `aiChat` fields + +**Files changed:** + +- `src/pages/DocsPage.tsx` - Added Footer component and rendering logic +- `convex/pages.ts` - Updated getDocsLandingPage query return fields +- `convex/posts.ts` - Updated getDocsLandingPost query return fields + +--- + ## v2.15.2 Released January 8, 2026 @@ -175,7 +254,7 @@ Diff and patch code blocks now render with enhanced visualization powered by @pi - View toggle button to switch between modes - Theme-aware colors matching dark/light/tan/cloud themes - Copy button for copying raw diff content -- Automatic routing: Use ```diff or ```patch in markdown +- Automatic routing: Use `diff or `patch in markdown **New documentation:** @@ -318,6 +397,7 @@ semanticSearch: { ``` When disabled: + - Search modal shows only keyword search (no mode toggle) - Embedding generation skipped during sync (saves API costs) - Existing embeddings preserved in database (no data loss) @@ -346,12 +426,12 @@ Search now supports two modes accessible via Cmd+K: **When to use each mode:** -| Use Case | Mode | -|----------|------| -| Specific code, commands, exact phrases | Keyword | +| Use Case | Mode | +| ----------------------------------------- | -------- | +| Specific code, commands, exact phrases | Keyword | | Conceptual questions ("how do I deploy?") | Semantic | -| Need to highlight matches on page | Keyword | -| Not sure of exact terminology | Semantic | +| Need to highlight matches on page | Keyword | +| Not sure of exact terminology | Semantic | **Configuration:** diff --git a/public/raw/contact.md b/public/raw/contact.md index 5d197e4..db332e8 100644 --- a/public/raw/contact.md +++ b/public/raw/contact.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- You found the contact page. Nice diff --git a/public/raw/docs-ask-ai.md b/public/raw/docs-ask-ai.md index fe9a23b..a9b6ce9 100644 --- a/public/raw/docs-ask-ai.md +++ b/public/raw/docs-ask-ai.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Ask AI diff --git a/public/raw/docs-configuration.md b/public/raw/docs-configuration.md index f6bc853..b4f5b1e 100644 --- a/public/raw/docs-configuration.md +++ b/public/raw/docs-configuration.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Configuration diff --git a/public/raw/docs-content.md b/public/raw/docs-content.md index 1a049e9..5de1cc0 100644 --- a/public/raw/docs-content.md +++ b/public/raw/docs-content.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Content diff --git a/public/raw/docs-dashboard.md b/public/raw/docs-dashboard.md index 7a1772b..2a1340f 100644 --- a/public/raw/docs-dashboard.md +++ b/public/raw/docs-dashboard.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Dashboard @@ -367,6 +367,63 @@ This creates a file in `content/blog/` that requires syncing. **Note:** The dashboard provides a UI for these commands. When the sync server is running (`npm run sync-server`), you can execute commands directly from the dashboard with real-time output. Otherwise, the dashboard shows commands in a modal for copying to your terminal. +### Version control + +The dashboard includes a Sync version control system that tracks changes to posts, pages, home content, and footer content. + +**Features:** + +- 3-day version history for all content +- Toggle to enable/disable version control +- View version history with unified diff visualization +- Preview previous versions +- One-click restore with automatic backup +- Automatic cleanup of versions older than 3 days + +**Enabling version control:** + +1. Navigate to Dashboard > Config +2. Find the "Version Control" card +3. Toggle "Enable version control" on + +When enabled, versions are captured: + +- Before sync updates (from markdown files) +- Before dashboard edits (Save Changes button) +- Before restoring a previous version + +**Viewing version history:** + +1. Open any post or page in the Dashboard editor +2. Click the clock (History) button in the editor toolbar +3. Select a version from the list to view details +4. Toggle between "Diff" and "Preview" modes +5. Click "Restore This Version" to revert + +**How it works:** + +- Versions are stored in the `contentVersions` table +- Settings stored in `versionControlSettings` table (database, not config file) +- Cleanup runs daily at 3:00 AM UTC via cron job +- Restore creates a backup of current content before reverting +- Uses existing DiffCodeBlock component for diff visualization + +**Version sources:** + +| Source | When created | +| --------- | ---------------------------- | +| sync | Before markdown sync updates | +| dashboard | Before dashboard edits | +| restore | Before restoring a version | + +**Stats display:** + +The Version Control card in Config shows: + +- Total version count +- Post versions count +- Page versions count + ### Environment variables **Convex Environment Variables:** @@ -386,7 +443,7 @@ npx convex env set VARIABLE_NAME value **Local Environment Variables (.env.local):** -| Variable | Description | -| ------------------- | ------------------------------------------ | -| `VITE_CONVEX_URL` | Your Convex deployment URL (auto-created) | -| `FIRECRAWL_API_KEY` | For CLI import command only | \ No newline at end of file +| Variable | Description | +| ------------------- | ----------------------------------------- | +| `VITE_CONVEX_URL` | Your Convex deployment URL (auto-created) | +| `FIRECRAWL_API_KEY` | For CLI import command only | \ No newline at end of file diff --git a/public/raw/docs-deployment.md b/public/raw/docs-deployment.md index 8dbc384..c65d56a 100644 --- a/public/raw/docs-deployment.md +++ b/public/raw/docs-deployment.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Deployment diff --git a/public/raw/docs-frontmatter.md b/public/raw/docs-frontmatter.md index eb6b22b..eb55613 100644 --- a/public/raw/docs-frontmatter.md +++ b/public/raw/docs-frontmatter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Frontmatter diff --git a/public/raw/docs-search.md b/public/raw/docs-search.md index 108c212..31ba94b 100644 --- a/public/raw/docs-search.md +++ b/public/raw/docs-search.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Keyword Search diff --git a/public/raw/docs-semantic-search.md b/public/raw/docs-semantic-search.md index 4ac5a50..010eb53 100644 --- a/public/raw/docs-semantic-search.md +++ b/public/raw/docs-semantic-search.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Semantic Search diff --git a/public/raw/documentation.md b/public/raw/documentation.md index 3c0a37a..e63fbc5 100644 --- a/public/raw/documentation.md +++ b/public/raw/documentation.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- ## Getting started diff --git a/public/raw/footer.md b/public/raw/footer.md index 6730fa2..f810945 100644 --- a/public/raw/footer.md +++ b/public/raw/footer.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com). Read the [project on GitHub](https://github.com/waynesutton/markdown-site) to fork and deploy your own. View [real-time site stats](/stats). diff --git a/public/raw/home-intro.md b/public/raw/home-intro.md index 2b53ec5..28b1ee5 100644 --- a/public/raw/home-intro.md +++ b/public/raw/home-intro.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- An open-source publishing framework built for AI agents and developers to ship **[docs](/docs)**, or **[blogs](/blog)** or **[websites](/)**. diff --git a/public/raw/index.md b/public/raw/index.md index 8409c4e..f7989a6 100644 --- a/public/raw/index.md +++ b/public/raw/index.md @@ -76,6 +76,7 @@ agents. --> - **[Footer](/raw/footer.md)** - **[Home Intro](/raw/home-intro.md)** - **[Documentation](/raw/documentation.md)** +- **[Dashboard](/raw/docs-dashboard.md)** - **[About](/raw/about.md)** - An open-source publishing framework built for AI agents and developers to ship websites, docs, or blogs. - **[Ask AI](/raw/docs-ask-ai.md)** - **[Content](/raw/docs-content.md)** @@ -86,7 +87,6 @@ agents. --> - **[Contact](/raw/contact.md)** - **[Configuration](/raw/docs-configuration.md)** - **[Changelog](/raw/changelog.md)** -- **[Dashboard](/raw/docs-dashboard.md)** - **[Deployment](/raw/docs-deployment.md)** - **[Newsletter](/raw/newsletter.md)** diff --git a/public/raw/newsletter.md b/public/raw/newsletter.md index 4569084..e84ca85 100644 --- a/public/raw/newsletter.md +++ b/public/raw/newsletter.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- # Newsletter Demo Page diff --git a/public/raw/projects.md b/public/raw/projects.md index 3de86ef..e0e8903 100644 --- a/public/raw/projects.md +++ b/public/raw/projects.md @@ -2,7 +2,7 @@ --- Type: page -Date: 2026-01-09 +Date: 2026-01-10 --- This markdown framework is open source and built to be extended. Here is what ships out of the box. diff --git a/src/components/VersionHistoryModal.tsx b/src/components/VersionHistoryModal.tsx new file mode 100644 index 0000000..a0d3910 --- /dev/null +++ b/src/components/VersionHistoryModal.tsx @@ -0,0 +1,238 @@ +import { useState, useMemo } from "react"; +import { useQuery, useMutation } from "convex/react"; +import { api } from "../../convex/_generated/api"; +import { Id } from "../../convex/_generated/dataModel"; +import { X, History, RotateCcw, Eye, FileCode } from "lucide-react"; +import { createPatch } from "diff"; +import DiffCodeBlock from "./DiffCodeBlock"; + +interface VersionHistoryModalProps { + isOpen: boolean; + onClose: () => void; + contentType: "post" | "page"; + contentId: string; + currentContent: string; + currentTitle: string; +} + +export default function VersionHistoryModal({ + isOpen, + onClose, + contentType, + contentId, + currentContent, + currentTitle, +}: VersionHistoryModalProps) { + const [selectedVersionId, setSelectedVersionId] = useState( + null + ); + const [viewMode, setViewMode] = useState<"preview" | "diff">("diff"); + const [isRestoring, setIsRestoring] = useState(false); + + const versions = useQuery(api.versions.getVersionHistory, { + contentType, + contentId, + }); + + const selectedVersion = useQuery( + api.versions.getVersion, + selectedVersionId + ? { versionId: selectedVersionId as Id<"contentVersions"> } + : "skip" + ); + + const restoreVersionMutation = useMutation(api.versions.restoreVersion); + + // Generate diff patch when a version is selected + const diffPatch = useMemo(() => { + if (!selectedVersion) return ""; + return createPatch( + "content.md", + selectedVersion.content, + currentContent, + `Version from ${new Date(selectedVersion.createdAt).toLocaleString()}`, + "Current Version" + ); + }, [selectedVersion, currentContent]); + + const handleRestore = async () => { + if (!selectedVersionId) return; + + setIsRestoring(true); + try { + const result = await restoreVersionMutation({ + versionId: selectedVersionId as Id<"contentVersions">, + }); + if (result.success) { + onClose(); + // The page will automatically update via Convex real-time subscription + } + } finally { + setIsRestoring(false); + } + }; + + const formatDate = (timestamp: number) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffHours / 24); + + if (diffHours < 1) { + const diffMins = Math.floor(diffMs / (1000 * 60)); + return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`; + } else if (diffHours < 24) { + return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`; + } else if (diffDays < 3) { + return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + } + return date.toLocaleString(); + }; + + const getSourceLabel = (source: string) => { + switch (source) { + case "sync": + return "Sync"; + case "dashboard": + return "Dashboard"; + case "restore": + return "Restore"; + default: + return source; + } + }; + + if (!isOpen) return null; + + return ( +
+
e.stopPropagation()}> +
+
+ + Version History + + {currentTitle} ({contentType}) + +
+ +
+ +
+
+
+ Previous Versions + + {versions?.length || 0} version + {versions?.length !== 1 ? "s" : ""} + +
+ + {versions?.length === 0 && ( +
+

No previous versions available.

+

+ Versions are created when content is edited. +

+
+ )} + + {versions?.map((version) => ( +
setSelectedVersionId(version._id)} + > +
+ + {formatDate(version.createdAt)} + + + {getSourceLabel(version.source)} + +
+
{version.title}
+
{version.contentPreview}
+
+ ))} +
+ +
+ {!selectedVersion ? ( +
+

Select a version to view details

+
+ ) : ( + <> +
+
+ + +
+
+ + {new Date(selectedVersion.createdAt).toLocaleString()} + +
+
+ +
+ {viewMode === "diff" ? ( +
+ +
+ ) : ( +
+

{selectedVersion.title}

+ {selectedVersion.description && ( +

+ {selectedVersion.description} +

+ )} +
+                        {selectedVersion.content}
+                      
+
+ )} +
+ + )} +
+
+ +
+ + +
+
+
+ ); +} diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 93aa815..0284a43 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -65,6 +65,7 @@ import { } from "@phosphor-icons/react"; import siteConfig from "../config/siteConfig"; import AIChatView from "../components/AIChatView"; +import VersionHistoryModal from "../components/VersionHistoryModal"; import { isWorkOSConfigured } from "../utils/workos"; // Always import auth components - they're only used when WorkOS is configured import { @@ -1880,6 +1881,8 @@ function EditorView({ }) { const [copied, setCopied] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [showVersionHistory, setShowVersionHistory] = useState(false); + const versionControlEnabled = useQuery(api.versions.isEnabled); const [sidebarWidth, setSidebarWidth] = useState(() => { const saved = localStorage.getItem("dashboard-sidebar-width"); return saved ? Number(saved) : 280; @@ -1976,6 +1979,16 @@ function EditorView({ {copied ? : } {copied ? "Copied" : "Copy"} + {versionControlEnabled && ( + + )}