Add a complete fork configuration system that allows users to set up their forked site with a single command or follow manual instructions. ## New files - FORK_CONFIG.md: Comprehensive guide with two setup options - Option 1: Automated JSON config + npm run configure - Option 2: Manual step-by-step instructions with code snippets - AI agent prompt for automated updates - fork-config.json.example: JSON template with all configuration fields - Site info (name, title, description, URL, domain) - GitHub and contact details - Creator section for footer links - Optional feature toggles (logo gallery, GitHub graph, blog page) - Theme selection - scripts/configure-fork.ts: Automated configuration script - Reads fork-config.json and applies changes to all files - Updates 11 configuration files in one command - Type-safe with ForkConfig interface - Detailed console output showing each file updated ## Updated files - package.json: Added configure script (npm run configure) - .gitignore: Added fork-config.json to keep user config local - files.md: Added new fork configuration files - changelog.md: Added v1.18.0 entry - changelog-page.md: Added v1.18.0 section with full details - TASK.md: Updated status and completed tasks - README.md: Replaced Files to Update section with Fork Configuration - content/blog/setup-guide.md: Added Fork Configuration Options section - content/pages/docs.md: Added Fork Configuration section - content/pages/about.md: Added fork configuration mention - content/blog/fork-configuration-guide.md: New featured blog post ## Files updated by configure script | File | What it updates | | ----------------------------------- | -------------------------------------- | | src/config/siteConfig.ts | Site name, bio, GitHub, features | | src/pages/Home.tsx | Intro paragraph, footer links | | src/pages/Post.tsx | SITE_URL, SITE_NAME constants | | convex/http.ts | SITE_URL, SITE_NAME constants | | convex/rss.ts | SITE_URL, SITE_TITLE, SITE_DESCRIPTION | | 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 | | public/.well-known/ai-plugin.json | Plugin metadata | | src/context/ThemeContext.tsx | Default theme | ## Usage Automated: cp fork-config.json.example fork-config.json # Edit fork-config.json npm run configure Manual: Follow FORK_CONFIG.md step-by-step guide
38 KiB
Setup Guide - Fork and Deploy Your Own Markdown Framework
Step-by-step guide to fork this markdown sync framework, set up Convex backend, and deploy to Netlify in under 10 minutes.
Type: post Date: 2025-01-14 Reading time: 8 min read Tags: convex, netlify, tutorial, deployment
Fork and Deploy Your Own Markdown Framework
This guide walks you through forking this markdown framework, setting up your Convex backend, and deploying to Netlify. The entire process takes about 10 minutes.
How publishing works: Once deployed, you write posts in markdown, run npm run sync for development or npm run sync:prod for production, and they appear on your live site immediately. No rebuild or redeploy needed. Convex handles real-time data sync, so all connected browsers update automatically.
Table of Contents
- Fork and Deploy Your Own Markdown Framework
- Table of Contents
- Prerequisites
- Step 1: Fork the Repository
- Step 2: Set Up Convex
- Step 3: Sync Your Blog Posts
- Step 4: Run Locally
- Step 5: Get Your Convex HTTP URL
- Step 6: Verify Edge Functions
- Step 7: Deploy to Netlify
- Step 8: Set Up Production Convex
- Writing Blog Posts
- Customizing Your Framework
- Files to Update When Forking
- Site title and description metadata
- Update Backend Configuration
- Change the Favicon
- Change the Site Logo
- Change the Default Open Graph Image
- Update Site Configuration
- Featured Section
- GitHub Contributions Graph
- Logo Gallery
- Blog page
- Scroll-to-top button
- Change the Default Theme
- Change the Font
- Change Font Sizes
- Add Static Pages (Optional)
- Update SEO Meta Tags
- Update llms.txt and robots.txt
- Search
- Real-time Stats
- Mobile Navigation
- Copy Page Dropdown
- API Endpoints
- Import External Content
- Troubleshooting
- Project Structure
- Write Page
- Next Steps
Prerequisites
Before you start, make sure you have:
- Node.js 18 or higher installed
- A GitHub account
- A Convex account (free at convex.dev)
- A Netlify account (free at netlify.com)
Step 1: Fork the Repository
Fork the repository to your GitHub account:
# Clone your forked repo
git clone https://github.com/waynesutton/markdown-site.git
cd markdown-site
# Install dependencies
npm install
Step 2: Set Up Convex
Convex is the backend that stores your blog posts and serves the API endpoints.
Create a Convex Project
Run the Convex development command:
npx convex dev
This will:
- Prompt you to log in to Convex (opens browser)
- Ask you to create a new project or select an existing one
- Generate a
.env.localfile with yourVITE_CONVEX_URL
Keep this terminal running during development. It syncs your Convex functions automatically.
Verify the Schema
The schema is already defined in convex/schema.ts:
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
posts: defineTable({
slug: v.string(),
title: v.string(),
description: v.string(),
content: v.string(),
date: v.string(),
published: v.boolean(),
tags: v.array(v.string()),
readTime: v.optional(v.string()),
image: v.optional(v.string()),
excerpt: v.optional(v.string()),
featured: v.optional(v.boolean()),
featuredOrder: v.optional(v.number()),
lastSyncedAt: v.number(),
})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"]),
pages: defineTable({
slug: v.string(),
title: v.string(),
content: v.string(),
published: v.boolean(),
order: v.optional(v.number()),
excerpt: v.optional(v.string()),
image: v.optional(v.string()),
featured: v.optional(v.boolean()),
featuredOrder: v.optional(v.number()),
lastSyncedAt: v.number(),
})
.index("by_slug", ["slug"])
.index("by_published", ["published"])
.index("by_featured", ["featured"]),
viewCounts: defineTable({
slug: v.string(),
count: v.number(),
}).index("by_slug", ["slug"]),
});
Step 3: Sync Your Blog Posts
Blog posts live in content/blog/ as markdown files. Sync them to Convex:
npm run sync
This reads all markdown files, parses the frontmatter, and uploads them to your Convex database.
Step 4: Run Locally
Start the development server:
npm run dev
Open http://localhost:5173 to see your blog.
Step 5: Get Your Convex HTTP URL
Your Convex deployment has two URLs:
- Client URL:
https://your-deployment.convex.cloud(for the React app) - HTTP URL:
https://your-deployment.convex.site(for API endpoints)
Find your deployment name in the Convex dashboard or check .env.local:
# Your .env.local contains something like:
VITE_CONVEX_URL=https://happy-animal-123.convex.cloud
The HTTP URL uses .convex.site instead of .convex.cloud:
https://happy-animal-123.convex.site
Step 6: Verify Edge Functions
The blog uses Netlify Edge Functions to dynamically proxy RSS, sitemap, and API requests to your Convex HTTP endpoints. No manual URL configuration is needed.
Edge functions in netlify/edge-functions/:
rss.ts- Proxies/rss.xmland/rss-full.xmlsitemap.ts- Proxies/sitemap.xmlapi.ts- Proxies/api/postsand/api/postbotMeta.ts- Serves Open Graph HTML to social media crawlers
These functions automatically read VITE_CONVEX_URL from your environment and convert it to the Convex HTTP site URL (.cloud becomes .site).
Step 7: Deploy to Netlify
For detailed Convex + Netlify integration, see the official Convex Netlify Deployment Guide.
Option A: Netlify CLI
# Install Netlify CLI
npm install -g netlify-cli
# Login to Netlify
netlify login
# Initialize site
netlify init
# Deploy
npm run deploy
Option B: Netlify Dashboard
- Go to app.netlify.com
- Click "Add new site" then "wImport an existing project"
- Connect your GitHub repository
- Configure build settings:
- Build command:
npm ci --include=dev && npx convex deploy --cmd 'npm run build' - Publish directory:
dist
- Build command:
- Add environment variables:
CONVEX_DEPLOY_KEY: Generate from Convex Dashboard > Project Settings > Deploy KeyVITE_CONVEX_URL: Your production Convex URL (e.g.,https://your-deployment.convex.cloud)
- Click "Deploy site"
The CONVEX_DEPLOY_KEY deploys functions at build time. The VITE_CONVEX_URL is required for edge functions to proxy RSS, sitemap, and API requests at runtime.
Netlify Build Configuration
The netlify.toml file includes the correct build settings:
[build]
command = "npm ci --include=dev && npx convex deploy --cmd 'npm run build'"
publish = "dist"
[build.environment]
NODE_VERSION = "20"
Key points:
npm ci --include=devforces devDependencies to install even whenNODE_ENV=production- The build script uses
npx vite buildto resolve vite from node_modules @types/nodeis required for TypeScript to recognizeprocess.env
Step 8: Set Up Production Convex
For production, deploy your Convex functions:
npx convex deploy
This creates a production deployment. Update your Netlify environment variable with the production URL if different.
Writing Blog Posts
Create new posts in content/blog/:
---
title: "Your Post Title"
description: "A brief description for SEO and social sharing"
date: "2025-01-15"
slug: "your-post-url"
published: true
tags: ["tag1", "tag2"]
readTime: "5 min read"
image: "/images/my-post-image.png"
---
Your markdown content here...
Frontmatter Fields
| Field | Required | Description |
|---|---|---|
title |
Yes | Post title |
description |
Yes | Short description for SEO |
date |
Yes | Publication date (YYYY-MM-DD) |
slug |
Yes | URL path (must be unique) |
published |
Yes | Set to true to publish |
tags |
Yes | Array of topic tags |
readTime |
No | Estimated reading time |
image |
No | Header/Open Graph image URL |
excerpt |
No | Short excerpt for card view |
featured |
No | Set true to show in featured section |
featuredOrder |
No | Order in featured section (lower = first) |
Adding Images
Place images in public/images/ and reference them in your posts:
Header/OG Image (in frontmatter):
image: "/images/my-header.png"
This image appears when sharing on social media. Recommended: 1200x630 pixels.
Inline Images (in content):

External Images:

Images require git deploy. Images are served as static files from your repository, not synced to Convex. After adding images to public/images/:
- Commit the image files to git
- Push to GitHub
- Wait for Netlify to rebuild
The npm run sync command only syncs markdown text content. Images are deployed when Netlify builds your site.
Sync After Adding Posts
After adding or editing posts, sync to Convex.
Development sync:
npm run sync
Production sync:
First, create .env.production.local in your project root:
VITE_CONVEX_URL=https://your-prod-deployment.convex.cloud
Get your production URL from the Convex Dashboard by selecting your project and switching to the Production deployment.
Then sync:
npm run sync:prod
Environment Files
| File | Purpose | Created by |
|---|---|---|
.env.local |
Dev deployment URL | npx convex dev (automatic) |
.env.production.local |
Prod deployment URL | You (manual) |
Both files are gitignored. Each developer creates their own local environment files.
When to Sync vs Deploy
| What you're changing | Command | Timing |
|---|---|---|
Blog posts in content/blog/ |
npm run sync |
Instant (no rebuild) |
Pages in content/pages/ |
npm run sync |
Instant (no rebuild) |
| Featured items (via frontmatter) | npm run sync |
Instant (no rebuild) |
| Import external URL | npm run import then sync |
Instant (no rebuild) |
Images in public/images/ |
Git commit + push | Requires rebuild |
siteConfig in Home.tsx |
Redeploy | Requires rebuild |
| Logo gallery config | Redeploy | Requires rebuild |
| React components/styles | Redeploy | Requires rebuild |
Markdown content syncs instantly via Convex. Images and source code require pushing to GitHub for Netlify to rebuild.
Featured items can now be controlled via markdown frontmatter. Add featured: true and featuredOrder: 1 to any post or page, then run npm run sync.
Customizing Your Framework
Fork Configuration Options
After forking, you have two options to configure your site:
Option 1: Automated (Recommended)
Run a single command to configure all files automatically:
# Copy the example config
cp fork-config.json.example fork-config.json
# Edit fork-config.json with your site information
# Then apply all changes
npm run configure
The fork-config.json file includes:
{
"siteName": "Your Site Name",
"siteTitle": "Your Tagline",
"siteDescription": "Your site description.",
"siteUrl": "https://yoursite.netlify.app",
"siteDomain": "yoursite.netlify.app",
"githubUsername": "yourusername",
"githubRepo": "your-repo-name",
"contactEmail": "you@example.com",
"creator": {
"name": "Your Name",
"twitter": "https://x.com/yourhandle",
"linkedin": "https://www.linkedin.com/in/yourprofile/",
"github": "https://github.com/yourusername"
},
"bio": "Your bio text here.",
"theme": "tan"
}
This updates all 11 configuration files in one step. See FORK_CONFIG.md for the full JSON schema.
Option 2: Manual
Follow the step-by-step guide in FORK_CONFIG.md to update each file manually. The guide includes code snippets for each file and an AI agent prompt for assisted configuration.
Files to Update When Forking
| File | What to update |
|---|---|
src/config/siteConfig.ts |
Site name, title, intro, bio, blog page, logo gallery, GitHub contributions |
src/pages/Home.tsx |
Intro paragraph text, footer links |
convex/http.ts |
SITE_URL, SITE_NAME, description strings (3 locations) |
convex/rss.ts |
SITE_URL, SITE_TITLE, SITE_DESCRIPTION (RSS feeds) |
src/pages/Post.tsx |
SITE_URL, SITE_NAME, DEFAULT_OG_IMAGE (OG tags) |
index.html |
Title, meta description, OG tags, JSON-LD |
public/llms.txt |
Site name, URL, description, topics |
public/robots.txt |
Sitemap URL and header comment |
public/openapi.yaml |
API title, server URL, site name in examples |
public/.well-known/ai-plugin.json |
Site name, descriptions |
src/context/ThemeContext.tsx |
Default theme |
Site title and description metadata
These files contain the main site description text. Update them with your own tagline:
| File | What to change |
|---|---|
index.html |
meta description, og:description, twitter:description, JSON-LD |
README.md |
Main description at top of file |
src/config/siteConfig.ts |
name, title, and bio fields |
src/pages/Home.tsx |
Intro paragraph (hardcoded JSX with links) |
convex/http.ts |
SITE_NAME constant and description strings (3 locations) |
convex/rss.ts |
SITE_TITLE and SITE_DESCRIPTION constants |
public/llms.txt |
Header quote, Name, and Description fields |
public/openapi.yaml |
API title and example site name |
AGENTS.md |
Project overview section |
content/blog/about-this-blog.md |
Title, description, excerpt, and opening paragraph |
content/pages/about.md |
excerpt field and opening paragraph |
content/pages/docs.md |
Opening description paragraph |
Update Backend Configuration
These constants affect RSS feeds, API responses, sitemaps, and social sharing metadata.
convex/http.ts:
const SITE_URL = "https://your-site.netlify.app";
const SITE_NAME = "Your Site Name";
convex/rss.ts:
const SITE_URL = "https://your-site.netlify.app";
const SITE_TITLE = "Your Site Name";
const SITE_DESCRIPTION = "Your site description for RSS feeds.";
src/pages/Post.tsx:
const SITE_URL = "https://your-site.netlify.app";
const SITE_NAME = "Your Site Name";
const DEFAULT_OG_IMAGE = "/images/og-default.svg";
Change the Favicon
Replace public/favicon.svg with your own SVG icon. The default is a rounded square with the letter "m":
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<rect x="32" y="32" width="448" height="448" rx="96" ry="96" fill="#000000"/>
<text x="256" y="330" text-anchor="middle" font-size="300" font-weight="800" fill="#ffffff">m</text>
</svg>
To use a different letter or icon, edit the SVG directly or replace the file.
Change the Site Logo
The logo appears on the homepage. Edit src/pages/Home.tsx:
const siteConfig = {
logo: "/images/logo.svg", // Set to null to hide the logo
// ...
};
Replace public/images/logo.svg with your own logo file. Recommended: SVG format, 512x512 pixels.
Change the Default Open Graph Image
The default OG image is used when a post does not have an image field in its frontmatter. Replace public/images/og-default.svg with your own image.
Recommended dimensions: 1200x630 pixels. Supported formats: PNG, JPG, or SVG.
Update the reference in src/pages/Post.tsx:
const DEFAULT_OG_IMAGE = "/images/og-default.svg";
Update Site Configuration
Edit src/config/siteConfig.ts to customize:
export default {
name: "Your Name",
title: "Your Title",
intro: "Your introduction...",
bio: "Your bio...",
// Blog page configuration
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
// Featured section options
featuredViewMode: "list", // 'list' or 'cards'
showViewToggle: true, // Let users switch between views
// Logo gallery (static grid or scrolling marquee with clickable links)
logoGallery: {
enabled: true, // Set false to hide
images: [
{ src: "/images/logos/logo1.svg", href: "https://example.com" },
{ src: "/images/logos/logo2.svg", href: "https://another.com" },
],
position: "above-footer", // or 'below-featured'
speed: 30, // Seconds for one scroll cycle
title: "Built with",
scrolling: false, // false = static grid, true = scrolling marquee
maxItems: 4, // Number of logos when scrolling is false
},
links: {
docs: "/setup-guide",
convex: "https://convex.dev",
},
};
Featured Section
The homepage featured section shows posts and pages marked with featured: true in their frontmatter. It supports two display modes:
- List view (default): Bullet list of links
- Card view: Grid of cards showing title and excerpt
Add a post to featured section:
Add these fields to any post or page frontmatter:
featured: true
featuredOrder: 1
excerpt: "A short description that appears on the card."
image: "/images/my-thumbnail.png"
Then run npm run sync. The post appears in the featured section instantly. No redeploy needed.
| Field | Description |
|---|---|
featured |
Set true to show in featured section |
featuredOrder |
Order in featured section (lower = first) |
excerpt |
Short text shown on card view |
image |
Thumbnail for card view (displays as square) |
Thumbnail images: In card view, the image field displays as a square thumbnail above the title. Non-square images are automatically cropped to center. Square thumbnails: 400x400px minimum (800x800px for retina).
Posts without images: Cards display without the image area. The card shows just the title and excerpt with adjusted padding.
Order featured items:
Use featuredOrder to control display order. Lower numbers appear first. Posts and pages are sorted together. Items without featuredOrder appear after numbered items, sorted by creation time.
Toggle view mode:
Users can toggle between list and card views using the icon button next to "Get started:". To change the default view, set featuredViewMode: "cards" in siteConfig.
GitHub Contributions Graph
Display your GitHub contribution activity on the homepage. Configure in siteConfig:
gitHubContributions: {
enabled: true, // Set to false to hide
username: "yourusername", // Your GitHub username
showYearNavigation: true, // Show arrows to navigate between years
linkToProfile: true, // Click graph to open GitHub profile
title: "GitHub Activity", // Optional title above the graph
},
| Option | Description |
|---|---|
enabled |
true to show, false to hide |
username |
Your GitHub username |
showYearNavigation |
Show prev/next year buttons |
linkToProfile |
Click graph to visit GitHub profile |
title |
Text above graph (set to undefined to hide) |
The graph displays with theme-aware colors that match each site theme (dark, light, tan, cloud). Uses the public github-contributions-api.jogruber.de API (no GitHub token required).
Logo Gallery
The homepage includes a logo gallery that can scroll infinitely or display as a static grid. Customize or disable it in siteConfig:
Disable the gallery:
logoGallery: {
enabled: false, // Set to false to hide
// ...
},
Replace with your own logos:
- Add your logo images to
public/images/logos/(SVG recommended) - Update the images array with your logos and links:
logoGallery: {
enabled: true,
images: [
{ src: "/images/logos/your-logo-1.svg", href: "https://example.com" },
{ src: "/images/logos/your-logo-2.svg", href: "https://anothersite.com" },
],
position: "above-footer",
speed: 30,
title: "Built with",
scrolling: false, // false = static grid, true = scrolling marquee
maxItems: 4, // Number of logos to show when scrolling is false
},
Each logo object supports:
src: Path to the logo image (required)href: URL to link to when clicked (optional)
Remove sample logos:
Delete the sample files from public/images/logos/ and clear the images array, or replace them with your own.
Configuration options:
| Option | Description |
|---|---|
enabled |
true to show, false to hide |
images |
Array of logo objects with src and optional href |
position |
'above-footer' or 'below-featured' |
speed |
Seconds for one scroll cycle (lower = faster) |
title |
Text above gallery (set to undefined to hide) |
scrolling |
true for infinite scroll, false for static grid |
maxItems |
Max logos to show when scrolling is false (default: 4) |
Display modes:
- Scrolling marquee (
scrolling: true): Infinite horizontal scroll animation. All logos display in a continuous loop. - Static grid (
scrolling: false): Centered grid showing the firstmaxItemslogos without animation.
Logos display in grayscale and colorize on hover.
Blog page
The site supports a dedicated blog page at /blog. Configure in src/config/siteConfig.ts:
blogPage: {
enabled: true, // Enable /blog route
showInNav: true, // Show in navigation
title: "Blog", // Nav link and page title
order: 0, // Nav order (lower = first)
},
displayOnHomepage: true, // Show posts on homepage
| Option | Description |
|---|---|
enabled |
Enable the /blog route |
showInNav |
Show Blog link in navigation |
title |
Text for nav link and page heading |
order |
Position in navigation (lower = first) |
displayOnHomepage |
Show post list on homepage |
Display options:
- Homepage only:
displayOnHomepage: true,blogPage.enabled: false - Blog page only:
displayOnHomepage: false,blogPage.enabled: true - Both:
displayOnHomepage: true,blogPage.enabled: true
Navigation order: The Blog link merges with page links and sorts by order. Pages use the order field in frontmatter. Set blogPage.order: 5 to position Blog after pages with order 0-4.
Scroll-to-top button
A scroll-to-top button appears after scrolling down on posts and pages. Configure it in src/components/Layout.tsx:
const scrollToTopConfig: Partial<ScrollToTopConfig> = {
enabled: true, // Set to false to disable
threshold: 300, // Show after scrolling 300px
smooth: true, // Smooth scroll animation
};
| Option | Description |
|---|---|
enabled |
true to show, false to hide |
threshold |
Pixels scrolled before button appears |
smooth |
true for smooth scroll, false for jump |
The button uses Phosphor ArrowUp icon and works with all four themes. It uses a passive scroll listener for performance.
Change the Default Theme
Edit src/context/ThemeContext.tsx:
const DEFAULT_THEME: Theme = "tan"; // Options: "dark", "light", "tan", "cloud"
Change the Font
The blog uses a serif font by default. To switch to sans-serif, edit src/styles/global.css:
body {
/* Sans-serif */
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Serif (default) */
font-family:
"New York",
-apple-system-ui-serif,
ui-serif,
Georgia,
serif;
}
Change Font Sizes
All font sizes use CSS variables defined in :root. Customize sizes by editing these variables in src/styles/global.css:
:root {
/* Base size scale */
--font-size-base: 16px;
--font-size-sm: 13px;
--font-size-lg: 17px;
--font-size-xl: 18px;
--font-size-2xl: 20px;
--font-size-3xl: 24px;
/* Component-specific (examples) */
--font-size-blog-content: 17px;
--font-size-post-title: 32px;
--font-size-nav-link: 14px;
}
Mobile responsive sizes are defined in a @media (max-width: 768px) block.
Add Static Pages (Optional)
Create optional pages like About, Projects, or Contact. These appear as navigation links in the top right corner.
- Create a
content/pages/directory - Add markdown files with frontmatter:
---
title: "About"
slug: "about"
published: true
order: 1
---
Your page content here...
| Field | Required | Description |
|---|---|---|
title |
Yes | Page title (shown in nav) |
slug |
Yes | URL path (e.g., /about) |
published |
Yes | Set true to show |
order |
No | Display order (lower = first) |
- Run
npm run syncto sync pages
Pages appear automatically in the navigation when published.
Update SEO Meta Tags
Edit index.html to update:
- Site title
- Meta description
- Open Graph tags
- JSON-LD structured data
Update llms.txt and robots.txt
Edit public/llms.txt and public/robots.txt with your site information.
Search
Your blog includes full text search with Command+K keyboard shortcut.
Using Search
Press Command+K (Mac) or Ctrl+K (Windows/Linux) to open the search modal. You can also click the search icon in the top navigation.
Features:
- Real-time results as you type
- Keyboard navigation with arrow keys
- Press Enter to select, Escape to close
- Result snippets with context around matches
- Distinguishes between posts and pages with type badges
- Works with all four themes
How It Works
Search uses Convex full text search indexes on the posts and pages tables. The search queries both title and content fields, deduplicates results, and sorts with title matches first.
Search is automatically available once you deploy. No additional configuration needed.
Real-time Stats
Your blog includes a real-time analytics page at /stats:
- Active visitors: See who is currently on your site and which pages they are viewing
- Total page views: All-time view count across the site
- Unique visitors: Count based on anonymous session IDs
- Views by page: Every page and post ranked by view count
Stats update automatically without refreshing. Powered by Convex subscriptions.
How it works:
- Page views are recorded as event records (not counters) to prevent write conflicts
- Active sessions use a heartbeat system (30 second interval)
- Sessions expire after 2 minutes of inactivity
- A cron job cleans up stale sessions every 5 minutes
- No personal data is stored (only anonymous UUIDs)
Mobile Navigation
On mobile and tablet screens (under 768px), a hamburger menu provides navigation. The menu slides out from the left with keyboard navigation (Escape to close) and a focus trap for accessibility. It auto-closes when you navigate to a new route.
Copy Page Dropdown
Each post and page includes a share dropdown with options for AI tools:
| Option | Description |
|---|---|
| Copy page | Copies formatted markdown to clipboard |
| Open in ChatGPT | Opens ChatGPT with article content |
| Open in Claude | Opens Claude with article content |
| Open in Perplexity | Opens Perplexity for research with content |
| View as Markdown | Opens raw .md file in new tab |
| Generate Skill | Downloads {slug}-skill.md for AI agent training |
Generate Skill formats the content as an AI agent skill file with metadata, when to use, and instructions sections.
Long content: If content exceeds URL limits, it copies to clipboard and opens the AI service in a new tab. Paste to continue.
API Endpoints
Your blog includes these API endpoints for search engines and AI:
| Endpoint | Description |
|---|---|
/stats |
Real-time site analytics |
/rss.xml |
RSS feed with descriptions |
/rss-full.xml |
RSS feed with full content |
/sitemap.xml |
Dynamic XML sitemap |
/api/posts |
JSON list of all posts |
/api/post?slug=xxx |
Single post as JSON |
/api/post?slug=xxx&format=md |
Single post as raw markdown |
/api/export |
Batch export all posts |
/raw/{slug}.md |
Static raw markdown file |
/.well-known/ai-plugin.json |
AI plugin manifest |
/openapi.yaml |
OpenAPI 3.0 specification |
/llms.txt |
AI agent discovery |
Import External Content
Use Firecrawl to import articles from external URLs as markdown posts:
npm run import https://example.com/article
Setup:
- Get an API key from firecrawl.dev
- Add to
.env.local:
FIRECRAWL_API_KEY=fc-your-api-key
The import script will:
- Scrape the URL and convert to markdown
- Create a draft post in
content/blog/locally - Extract title and description from the page
Why no npm run import:prod? The import command only creates local markdown files. It does not interact with Convex directly. After importing:
- Run
npm run syncto push to development - Run
npm run sync:prodto push to production
Imported posts are created as drafts (published: false). Review, edit, set published: true, then sync to your target environment.
Troubleshooting
Posts not appearing
- Check that
published: truein frontmatter - Run
npm run syncto sync posts to development - Run
npm run sync:prodto sync posts to production - Verify posts exist in Convex dashboard
RSS/Sitemap not working
- Verify
VITE_CONVEX_URLis set in Netlify environment variables - Check that Convex HTTP endpoints are deployed (
npx convex deploy) - Test the Convex HTTP URL directly:
https://your-deployment.convex.site/rss.xml - Verify edge functions exist in
netlify/edge-functions/
Build failures on Netlify
Common errors and fixes:
"vite: not found" or "Cannot find package 'vite'"
Netlify sets NODE_ENV=production which skips devDependencies. Fix by using npm ci --include=dev in your build command:
[build]
command = "npm ci --include=dev && npx convex deploy --cmd 'npm run build'"
Also ensure your build script uses npx:
"build": "npx vite build"
"Cannot find name 'process'"
Add @types/node to devDependencies:
npm install --save-dev @types/node
General checklist:
- Verify
CONVEX_DEPLOY_KEYenvironment variable is set in Netlify - Check that
@types/nodeis in devDependencies - Ensure Node.js version is 20 or higher
- Verify build command includes
--include=dev
See netlify-deploy-fix.md for detailed troubleshooting.
Project Structure
markdown-site/
├── content/
│ ├── blog/ # Markdown posts
│ └── pages/ # Static pages (About, Docs, etc.)
├── convex/ # Convex backend functions
│ ├── http.ts # HTTP endpoints
│ ├── posts.ts # Post queries/mutations
│ ├── pages.ts # Page queries/mutations
│ ├── rss.ts # RSS feed generation
│ ├── stats.ts # Analytics functions
│ └── schema.ts # Database schema
├── netlify/
│ └── edge-functions/ # Netlify edge functions
│ ├── rss.ts # RSS proxy
│ ├── sitemap.ts # Sitemap proxy
│ ├── api.ts # API proxy
│ └── botMeta.ts # OG crawler detection
├── public/
│ ├── images/ # Static images
│ ├── raw/ # Generated raw markdown files
│ ├── robots.txt # Crawler rules
│ └── llms.txt # AI agent discovery
├── src/
│ ├── components/ # React components
│ ├── context/ # Theme context
│ ├── hooks/ # Custom hooks
│ ├── pages/ # Page components
│ └── styles/ # Global CSS
├── netlify.toml # Netlify configuration
└── package.json # Dependencies
Write Page
A markdown writing page is available at /write (not linked in navigation). Use it to draft content before saving to your markdown files.
Features:
- Three-column Cursor docs-style layout
- Content type selector (Blog Post or Page) with dynamic frontmatter templates
- Frontmatter field reference with individual copy buttons
- Font switcher (Serif/Sans-serif)
- Theme toggle matching site themes
- Word, line, and character counts
- localStorage persistence for content, type, and font preference
- Works with Grammarly and browser spellcheck
Workflow:
- Go to
yourdomain.com/write - Select content type (Blog Post or Page)
- Write your content using the frontmatter reference
- Click "Copy All" to copy the markdown
- Save to
content/blog/orcontent/pages/ - Run
npm run syncornpm run sync:prod
Content is stored in localStorage only and not synced to the database. Refreshing the page preserves your content, but clearing browser data will lose it.
Next Steps
After deploying:
- Add your own blog posts
- Customize the theme colors in
global.css - Update the featured essays list
- Submit your sitemap to Google Search Console
- Share your first post
Your blog is now live with real-time updates, SEO optimization, and AI-friendly APIs. Every time you sync new posts, they appear immediately without redeploying.