After forking this repo, update these files with your site information. Choose one of three options:
Important: Keep your fork-config.json file after configuring. The sync:discovery commands will use it to update discovery files (AGENTS.md, CLAUDE.md, public/llms.txt) with your configured values. Discovery sync also fetches wiki pages from Convex and includes a wiki knowledge base section in llms.txt and AGENTS.md, and copies AGENTS.md to public/AGENTS.md for web access.
If you want the fastest browser-first start:
- Click Use this template
- Create your repo from the template
- Clone your new repo locally
- Run:
npm install
npx convex dev --once
npm run sync
npm run deployUse the options below to customize app branding and behavior after the first deploy.
Default mode:
auth.mode: "convex-auth"hosting.mode: "convex-self-hosted"media.provider: "convex"
Legacy mode:
auth.mode: "workos"for WorkOS compatibilityhosting.mode: "netlify"for Netlify compatibilitymedia.provider: "convexfs"ormedia.provider: "r2"for optional media backends
Local fallback mode:
auth.mode: "none"for local development only
Example:
{
"auth": { "mode": "convex-auth" },
"hosting": { "mode": "convex-self-hosted" },
"media": { "provider": "convex" }
}Run a single command to scaffold and configure your project with an interactive wizard:
npx create-markdown-sync my-siteThe interactive wizard will:
- Clone the repository
- Walk through all configuration options (site name, URL, features, etc.)
- Install dependencies
- Set up Convex backend (opens browser for login)
- Run initial content sync
- Open your site in the browser
cd my-site
npm run dev # Start dev server at localhost:5173
npm run sync # Sync content changesnpx create-markdown-sync my-site --force # Overwrite existing directory
npx create-markdown-sync my-site --skip-convex # Skip Convex setup
npx create-markdown-sync my-site --skip-open # Don't open browser after setupRun a single command to configure all files automatically.
cp fork-config.json.example fork-config.jsonThe file fork-config.json is gitignored, so your configuration stays local and is not committed. The .example file remains as a template.
Keep this file: Even after running npm run configure, keep the fork-config.json file. Future sync commands will use it to maintain your configuration.
{
"siteName": "Your Site Name",
"siteTitle": "Your Tagline",
"siteDescription": "A one-sentence description of your site.",
"siteUrl": "https://yoursite.example.com",
"siteDomain": "yoursite.example.com",
"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"
}npm run configureThis updates all 14 configuration files automatically:
src/config/siteConfig.ts(site name, bio, GitHub username, gitHubRepo config, default theme)src/pages/Home.tsx(intro paragraph, footer links)src/pages/Post.tsx(SITE_URL, SITE_NAME constants)src/pages/DocsPage.tsx(SITE_URL constant for CopyPageDropdown)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)scripts/send-newsletter.ts(default SITE_URL)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)
git diff # Review changes
npx convex dev # Start Convex (if not running)
npm run sync # Sync content
npm run dev # Test locally
npm run deploy # One-shot Convex self-hosted deployEdit each file individually following the guide below.
| 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 |
Update the main site configuration:
export const siteConfig: SiteConfig = {
name: "YOUR SITE NAME",
title: "YOUR TAGLINE",
logo: "/images/logo.svg", // or null to hide
intro: null,
bio: `YOUR BIO TEXT HERE.`,
// Featured section
featuredViewMode: "cards", // 'list' or 'cards'
featuredTitle: "Get started:", // Featured section title (e.g., "Get started:", "Featured", "Popular")
showViewToggle: true,
// Logo gallery (set enabled: false to hide)
logoGallery: {
enabled: true,
images: [
{ src: "/images/logos/your-logo.svg", href: "https://example.com" },
],
position: "above-footer",
speed: 30,
title: "Built with",
scrolling: false,
maxItems: 4,
},
// GitHub contributions graph
gitHubContributions: {
enabled: true,
username: "YOURUSERNAME",
showYearNavigation: true,
linkToProfile: true,
title: "GitHub Activity",
},
// Visitor map (stats page)
visitorMap: {
enabled: true,
title: "Live Visitors",
},
// Blog page
blogPage: {
enabled: true,
showInNav: true,
title: "Blog",
description: "All posts from the blog, sorted by date.",
order: 2,
},
// Posts display
postsDisplay: {
showOnHome: true,
showOnBlogPage: true,
},
// Homepage configuration
// Set any page or blog post to serve as the homepage
homepage: {
type: "default", // Options: "default" (standard Home component), "page" (use a static page), or "post" (use a blog post)
slug: undefined, // Required if type is "page" or "post" - the slug of the page/post to use
originalHomeRoute: "/home", // Route to access the original homepage when custom homepage is set
},
links: {
docs: "/setup-guide",
convex: "https://convex.dev",
netlify: "https://netlify.com",
},
// GitHub repository config (for AI service links)
// Used by ChatGPT, Claude, Perplexity "Open in AI" buttons
gitHubRepo: {
owner: "YOURUSERNAME", // GitHub username or organization
repo: "YOUR-REPO-NAME", // Repository name
branch: "main", // Default branch
contentPath: "public/raw", // Path to raw markdown files
},
// Stats page configuration (optional)
statsPage: {
enabled: true, // Global toggle for stats page
showInNav: true, // Show link in navigation (controlled via hardcodedNavItems)
},
// Image lightbox configuration (optional)
imageLightbox: {
enabled: true, // Enable click-to-magnify for images in posts/pages
},
// MCP Server configuration (optional)
mcpServer: {
enabled: true, // Global toggle for MCP server
endpoint: "/mcp", // Endpoint path
publicRateLimit: 50, // Requests per minute for public access
authenticatedRateLimit: 1000, // Requests per minute with API key
requireAuth: false, // Require API key for all requests
},
};Update the intro paragraph (lines 96-108):
<p className="home-intro">
YOUR SITE DESCRIPTION HERE.{" "}
<a
href="https://github.com/YOURUSERNAME/YOUR-REPO"
target="_blank"
rel="noopener noreferrer"
className="home-text-link"
>
Fork it
</a>
, customize it, ship it.
</p>Update the footer section (lines 203-271):
<section className="home-footer">
<p className="home-footer-text">
Built with{" "}
<a href={siteConfig.links.convex} target="_blank" rel="noopener noreferrer">
Convex
</a>{" "}
for real-time sync and deployed on{" "}
<a
href={siteConfig.links.netlify}
target="_blank"
rel="noopener noreferrer"
>
Netlify
</a>
. Read the{" "}
<a
href="https://github.com/YOURUSERNAME/YOUR-REPO"
target="_blank"
rel="noopener noreferrer"
>
project on GitHub
</a>{" "}
to fork and deploy your own. View{" "}
<a href="/stats" className="home-text-link">
real-time site stats
</a>
.
</p>
<p></p>
<br></br>
<p className="home-footer-text">
Created by{" "}
<a
href="https://x.com/YOURHANDLE"
target="_blank"
rel="noopener noreferrer"
>
YOUR NAME
</a>{" "}
with Convex, Cursor, and Claude. Follow on{" "}
<a
href="https://x.com/YOURHANDLE"
target="_blank"
rel="noopener noreferrer"
>
Twitter/X
</a>
,{" "}
<a
href="https://www.linkedin.com/in/YOURPROFILE/"
target="_blank"
rel="noopener noreferrer"
>
LinkedIn
</a>
, and{" "}
<a
href="https://github.com/YOURUSERNAME"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
.
</p>
</section>Update the site constants (lines 11-13):
const SITE_URL = "https://yoursite.example.com";
const SITE_NAME = "YOUR SITE NAME";
const DEFAULT_OG_IMAGE = "/images/og-default.svg";Update the site configuration (lines 9-10):
const SITE_URL = process.env.SITE_URL || "https://yoursite.example.com";
const SITE_NAME = "YOUR SITE NAME";Also update the generateMetaHtml function (lines 233-234):
const siteUrl = process.env.SITE_URL || "https://yoursite.example.com";
const siteName = "YOUR SITE NAME";Update the RSS configuration (lines 5-8):
const SITE_URL = process.env.SITE_URL || "https://yoursite.example.com";
const SITE_TITLE = "YOUR SITE NAME";
const SITE_DESCRIPTION = "YOUR SITE DESCRIPTION HERE.";Update all meta tags and JSON-LD structured data:
<!-- SEO Meta Tags -->
<meta name="description" content="YOUR SITE DESCRIPTION" />
<meta name="author" content="YOUR SITE NAME" />
<!-- Open Graph -->
<meta property="og:title" content="YOUR SITE NAME" />
<meta property="og:description" content="YOUR SITE DESCRIPTION" />
<meta property="og:url" content="https://yoursite.example.com/" />
<meta property="og:site_name" content="YOUR SITE NAME" />
<meta
property="og:image"
content="https://yoursite.example.com/images/og-default.svg"
/>
<!-- Twitter Card -->
<meta property="twitter:domain" content="yoursite.example.com" />
<meta property="twitter:url" content="https://yoursite.example.com/" />
<meta name="twitter:title" content="YOUR SITE NAME" />
<meta name="twitter:description" content="YOUR SITE DESCRIPTION" />
<meta
name="twitter:image"
content="https://yoursite.example.com/images/og-default.svg"
/>
<!-- JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "YOUR SITE NAME",
"url": "https://yoursite.example.com",
"description": "YOUR SITE DESCRIPTION"
}
</script>
<title>YOUR SITE TITLE</title>Update site information:
# Site Information
- Name: YOUR SITE NAME
- URL: https://yoursite.example.com
- Description: YOUR SITE DESCRIPTION
# Links
- GitHub: https://github.com/YOURUSERNAME/YOUR-REPO
Update the header and sitemap URL:
# robots.txt for YOUR SITE NAME
Sitemap: https://yoursite.example.com/sitemap.xml
Update API title and server URL:
info:
title: YOUR SITE NAME API
contact:
url: https://github.com/YOURUSERNAME/YOUR-REPO
servers:
- url: https://yoursite.example.comUpdate plugin metadata:
{
"name_for_human": "YOUR SITE NAME",
"name_for_model": "your_site_name",
"description_for_human": "YOUR SITE DESCRIPTION",
"contact_email": "you@example.com"
}Change the default theme in src/config/siteConfig.ts:
export const siteConfig: SiteConfig = {
// ... other config
defaultTheme: "tan", // Options: "dark", "light", "tan", "cloud"
};You can set any page or blog post to serve as your homepage instead of the default Home component.
{
"homepage": {
"type": "page",
"slug": "about",
"originalHomeRoute": "/home"
}
}In src/config/siteConfig.ts:
homepage: {
type: "page", // Options: "default", "page", or "post"
slug: "about", // Required if type is "page" or "post" - the slug of the page/post to use
originalHomeRoute: "/home", // Route to access the original homepage when custom homepage is set
},type:"default"(standard Home component),"page"(use a static page), or"post"(use a blog post)slug: The slug of the page or post to use (required if type is "page" or "post")originalHomeRoute: Route to access the original homepage (default: "/home")
- Custom homepage uses the page/post's full content and features (sidebar, copy dropdown, footer, etc.)
- Featured section is NOT shown on custom homepage (only on default Home component)
- SEO metadata comes from the page/post's frontmatter
- Original homepage remains accessible at
/home(or configured route) when custom homepage is set - Back button is hidden when a page/post is used as the homepage
Use a static page as homepage:
homepage: {
type: "page",
slug: "about",
originalHomeRoute: "/home",
},Use a blog post as homepage:
homepage: {
type: "post",
slug: "welcome-post",
originalHomeRoute: "/home",
},Switch back to default homepage:
homepage: {
type: "default",
slug: undefined,
originalHomeRoute: "/home",
},The newsletter feature integrates with AgentMail for email subscriptions and sending. It is disabled by default.
Set these in the Convex dashboard:
| Variable | Description |
|---|---|
AGENTMAIL_API_KEY |
Your AgentMail API key |
AGENTMAIL_INBOX |
Your inbox address (e.g., newsletter@mail.agentmail.to) |
{
"newsletter": {
"enabled": true,
"agentmail": {
"inbox": "newsletter@mail.agentmail.to"
},
"signup": {
"home": {
"enabled": true,
"position": "above-footer",
"title": "Stay Updated",
"description": "Get new posts delivered to your inbox."
},
"blogPage": {
"enabled": true,
"position": "above-footer",
"title": "Subscribe",
"description": "Get notified when new posts are published."
},
"posts": {
"enabled": true,
"position": "below-content",
"title": "Enjoyed this post?",
"description": "Subscribe for more updates."
}
}
}
}In src/config/siteConfig.ts:
newsletter: {
enabled: true, // Master switch for newsletter feature
agentmail: {
inbox: "newsletter@mail.agentmail.to",
},
signup: {
home: {
enabled: true,
position: "above-footer", // or "below-intro"
title: "Stay Updated",
description: "Get new posts delivered to your inbox.",
},
blogPage: {
enabled: true,
position: "above-footer", // or "below-posts"
title: "Subscribe",
description: "Get notified when new posts are published.",
},
posts: {
enabled: true,
position: "below-content",
title: "Enjoyed this post?",
description: "Subscribe for more updates.",
},
},
},Hide or show newsletter signup on specific posts using frontmatter:
---
title: My Post
newsletter: false # Hide newsletter signup on this post
---Or force show it even if posts default is disabled:
---
title: Special Offer Post
newsletter: true # Show newsletter signup on this post
---To send a newsletter for a specific post:
npm run newsletter:send setup-guideOr use the Convex CLI directly:
npx convex run newsletter:sendPostNewsletter '{"postSlug":"setup-guide","siteUrl":"https://yoursite.com","siteName":"Your Site"}'View subscriber count on the /stats page. Subscribers are stored in the newsletterSubscribers table in Convex.
The Newsletter Admin UI at /newsletter-admin provides a management interface for subscribers and sending newsletters.
Configuration:
In src/config/siteConfig.ts:
newsletterAdmin: {
enabled: true, // Enable /newsletter-admin route
showInNav: false, // Hide from navigation (access via direct URL)
},Features:
- View and search all subscribers
- Filter by status (all, active, unsubscribed)
- Delete subscribers
- Send blog posts as newsletters
- Write and send custom emails with markdown support
- View recent newsletter sends
- Email statistics dashboard
CLI Commands:
# Send a blog post to all subscribers
npm run newsletter:send <post-slug>
# Send weekly stats summary
npm run newsletter:send:statsConfigure developer notifications for subscriber events:
In src/config/siteConfig.ts:
newsletterNotifications: {
enabled: true, // Global toggle for notifications
newSubscriberAlert: true, // Send email when new subscriber signs up
weeklyStatsSummary: true, // Send weekly stats summary email
},Uses AGENTMAIL_CONTACT_EMAIL or AGENTMAIL_INBOX as recipient.
Automated weekly email with posts from the past 7 days:
In src/config/siteConfig.ts:
weeklyDigest: {
enabled: true, // Global toggle for weekly digest
dayOfWeek: 0, // 0 = Sunday, 6 = Saturday
subject: "Weekly Digest", // Email subject prefix
},Runs automatically via cron job every Sunday at 9:00 AM UTC.
The dashboard at /dashboard provides a centralized UI for managing content, configuring the site, and performing sync operations.
Configuration:
In src/config/siteConfig.ts:
dashboard: {
enabled: true, // Global toggle for dashboard page
requireAuth: true, // Keep true for production deployments
showInNav: true, // Show "Dashboard" nav item for admins
},Authentication:
Dashboard access is server-enforced and admin-only when authentication is enabled. Use:
auth.mode: "convex-auth"for the default path (GitHub OAuth)auth.mode: "workos"for legacy compatibility
When dashboard.requireAuth is true, users must be authenticated and present in the dashboardAdmins table to access /dashboard.
After forking, you must set up your own admin email to access the dashboard:
Step 1: Set up GitHub OAuth
- Go to https://github.com/settings/developers
- Create a new OAuth App
- Set Homepage URL to your frontend URL (e.g.,
http://localhost:5173) - Set Authorization callback URL to:
https://<your-deployment>.convex.site/api/auth/callback/github - Copy Client ID and Client Secret
Step 2: Set environment variables
npx convex env set AUTH_GITHUB_ID "your-github-client-id"
npx convex env set AUTH_GITHUB_SECRET "your-github-client-secret"
npx convex env set DASHBOARD_ADMIN_BOOTSTRAP_KEY "choose-a-long-random-secret"Step 3: Bootstrap your admin
npx convex run authAdmin:bootstrapDashboardAdmin \
'{"bootstrapKey":"choose-a-long-random-secret","email":"your-email@example.com"}'Step 4 (optional): Add strict email gate
For extra security, set a strict email gate that only allows one specific email:
npx convex env set DASHBOARD_PRIMARY_ADMIN_EMAIL "your-email@example.com"When a user signs in with GitHub but is not in the dashboardAdmins table:
- Homepage: Shows a dismissible notice with Sign Out and Dismiss buttons
- Dashboard route: Redirects to homepage with the notice
- Navigation: Dashboard link is hidden for non-admins
Non-admin users can browse all public content but cannot access /dashboard or modify any content.
Once you are an admin, grant access to others:
npx convex run authAdmin:grantDashboardAdmin '{"email":"colleague@example.com"}'WorkOS Setup:
To enable WorkOS authentication:
- Create a WorkOS account at workos.com
- Set
VITE_WORKOS_CLIENT_IDin your.env.localfile - Set
VITE_WORKOS_REDIRECT_URI(e.g.,http://localhost:5173/callback) - Add
WORKOS_CLIENT_IDto Convex environment variables - Configure redirect URI in WorkOS dashboard
- Set
auth.modeto"workos"and keepdashboard.requireAuth: trueinsiteConfig.ts
See How to setup WorkOS for complete setup instructions.
Features:
- Content management: Edit posts and pages with live preview
- Sync commands: Run sync operations from the browser
- Site configuration: Configure all settings via UI
- Newsletter management: Integrated subscriber and email management
- AI Agent: Writing assistance powered by Claude
- Analytics: Real-time stats dashboard
See How to use the Markdown sync dashboard for complete usage guide.
The dashboard includes a sync server feature that allows executing sync commands directly from the browser UI without opening a terminal.
Setup:
- Start the sync server locally:
npm run sync-server- The server runs on
localhost:3001and is automatically detected by the dashboard - Optional: Set
SYNC_TOKENenvironment variable for authentication
Features:
- Execute sync commands from dashboard UI
- Real-time output streaming in dashboard terminal view
- Server status indicator (online/offline)
- Whitelisted commands only (sync, sync:prod, sync:discovery, sync:discovery:prod, sync:wiki, sync:wiki:prod, sync:all, sync:all:prod)
Control access to the /stats route for viewing site analytics.
{
"statsPage": {
"enabled": true,
"showInNav": true
}
}In src/config/siteConfig.ts:
statsPage: {
enabled: true, // Global toggle for stats page
showInNav: true, // Show link in navigation (controlled via hardcodedNavItems)
},Note: Navigation visibility is controlled via hardcodedNavItems configuration. Set showInNav: false on the stats nav item to hide it.
Enable click-to-magnify functionality for images in blog posts and pages.
{
"imageLightbox": {
"enabled": true
}
}In src/config/siteConfig.ts:
imageLightbox: {
enabled: true, // Enable click-to-magnify for images
},Features:
- Click any image in a post/page to open in full-screen lightbox
- Dark backdrop with close button (X icon)
- Keyboard support: Press Escape to close
- Click outside image (backdrop) to close
- Alt text displayed as caption below image
- Images show pointer cursor (
zoom-in) when enabled
Enable AI-powered semantic search using OpenAI embeddings. When disabled, only keyword search is available.
{
"semanticSearch": {
"enabled": false
}
}In src/config/siteConfig.ts:
semanticSearch: {
enabled: true, // Enable semantic search (requires OPENAI_API_KEY)
},Requirements:
When enabled, set the OpenAI API key in Convex:
npx convex env set OPENAI_API_KEY sk-your-key-hereFeatures:
- Toggle between Keyword and Semantic modes in search modal (Cmd+K)
- Keyword search: exact word matching (instant, free)
- Semantic search: finds content by meaning (~300ms, ~$0.0001/query)
- Similarity scores displayed as percentages
- Embeddings generated automatically during
npm run sync
Default: enabled: false (keyword search only, no API key required)
See Semantic Search for detailed documentation.
HTTP-based Model Context Protocol server for AI tool integration (Cursor, Claude Desktop).
{
"mcpServer": {
"enabled": true,
"endpoint": "/mcp",
"publicRateLimit": 50,
"authenticatedRateLimit": 1000,
"requireAuth": false
}
}In src/config/siteConfig.ts:
mcpServer: {
enabled: true, // Global toggle for MCP server
endpoint: "/mcp", // Endpoint path
publicRateLimit: 50, // Requests per minute for public access
authenticatedRateLimit: 1000, // Requests per minute with API key
requireAuth: false, // Require API key for all requests
},Environment Variables:
Set MCP_API_KEY in Netlify environment variables for authenticated access.
Features:
- Accessible 24/7 at
https://yoursite.com/mcp - Public access with Netlify built-in rate limiting (50 req/min per IP)
- Optional API key authentication for higher limits (1000 req/min)
- Read-only access to blog posts, pages, homepage, and search
- 7 tools:
list_posts,get_post,list_pages,get_page,get_homepage,search_content,export_all - JSON-RPC 2.0 protocol over HTTP POST
See How to Use the MCP Server for client configuration examples.
Enable contact forms on any page or post via frontmatter. Messages are sent via AgentMail.
Set these in the Convex dashboard:
| Variable | Description |
|---|---|
AGENTMAIL_API_KEY |
Your AgentMail API key |
AGENTMAIL_INBOX |
Your inbox address for sending (e.g., newsletter@mail.agentmail.to) |
AGENTMAIL_CONTACT_EMAIL |
Optional: recipient for contact form messages (defaults to AGENTMAIL_INBOX) |
In src/config/siteConfig.ts:
contactForm: {
enabled: true, // Global toggle for contact form feature
title: "Get in Touch",
description: "Send us a message and we'll get back to you.",
},Note: Recipient email is configured via Convex environment variables (AGENTMAIL_CONTACT_EMAIL or AGENTMAIL_INBOX). Never hardcode email addresses in code.
Enable contact form on any page or post:
---
title: Contact Us
slug: contact
contactForm: true
---The form includes name, email, and message fields. Submissions are stored in Convex and sent via AgentMail to the configured recipient.
The footer component displays markdown content and can be configured globally or per-page.
{
"footer": {
"enabled": true,
"showOnHomepage": true,
"showOnPosts": true,
"showOnPages": true,
"showOnBlogPage": true,
"defaultContent": "Built with [Convex](https://convex.dev) for real-time sync."
}
}In src/config/siteConfig.ts:
footer: {
enabled: true, // Global toggle for footer
showOnHomepage: true, // Show footer on homepage
showOnPosts: true, // Default: show footer on blog posts
showOnPages: true, // Default: show footer on static pages
showOnBlogPage: true, // Show footer on /blog page
defaultContent: "...", // Default markdown content
},Frontmatter Override:
Set showFooter: false in post/page frontmatter to hide footer on specific pages. Set footer: "..." to provide custom markdown content.
Display social icons and copyright information below the main footer. Icons can also appear in the header.
{
"socialFooter": {
"enabled": true,
"showOnHomepage": true,
"showOnPosts": true,
"showOnPages": true,
"showOnBlogPage": true,
"showInHeader": true,
"socialLinks": [
{
"platform": "github",
"url": "https://github.com/yourusername/your-repo-name"
},
{
"platform": "twitter",
"url": "https://x.com/yourhandle"
}
],
"copyright": {
"siteName": "Your Site Name",
"showYear": true
}
}
}In src/config/siteConfig.ts:
socialFooter: {
enabled: true,
showOnHomepage: true,
showOnPosts: true,
showOnPages: true,
showOnBlogPage: true,
showInHeader: true, // Show social icons in header (left of search icon)
socialLinks: [
{ platform: "github", url: "https://github.com/username" },
{ platform: "twitter", url: "https://x.com/handle" },
{ platform: "linkedin", url: "https://linkedin.com/in/profile" },
],
copyright: {
siteName: "Your Site Name",
showYear: true, // Auto-updates to current year
},
},Supported Platforms: github, twitter, linkedin, instagram, youtube, tiktok, discord, website
Header Social Icons:
When showInHeader: true, social icons appear in the navigation header to the left of the search icon on desktop. This provides additional visibility for your social links while maintaining the footer placement.
Frontmatter Override:
Set showSocialFooter: false in post/page frontmatter to hide social footer on specific pages.
Enable a right sidebar on posts and pages that displays CopyPageDropdown at wide viewport widths.
{
"rightSidebar": {
"enabled": true,
"minWidth": 1135
}
}In src/config/siteConfig.ts:
rightSidebar: {
enabled: true, // Set to false to disable globally
minWidth: 1135, // Minimum viewport width to show sidebar
},Frontmatter Usage:
Enable right sidebar on specific posts/pages:
---
title: My Post
rightSidebar: true
---Features:
- Right sidebar appears at 1135px+ viewport width
- Contains CopyPageDropdown with sharing options
- Three-column layout: left sidebar (TOC), main content, right sidebar
- Hidden below 1135px, CopyPageDropdown returns to nav
Configure the AI writing assistant. The Dashboard AI Agent supports multiple providers (Anthropic, OpenAI, Google) and includes image generation.
{
"aiChat": {
"enabledOnWritePage": false,
"enabledOnContent": false
},
"aiDashboard": {
"enableImageGeneration": true,
"defaultTextModel": "claude-sonnet-4-20250514",
"textModels": [
{
"id": "claude-sonnet-4-20250514",
"name": "Claude Sonnet 4",
"provider": "anthropic"
},
{ "id": "gpt-4.1-mini", "name": "GPT-4.1 mini", "provider": "openai" },
{
"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"
}
]
}
}In src/config/siteConfig.ts:
aiChat: {
enabledOnWritePage: true, // Show AI chat toggle on /write page
enabledOnContent: true, // Allow AI chat on posts/pages via frontmatter
},
aiDashboard: {
enableImageGeneration: true, // Enable image generation tab in Dashboard AI Agent
defaultTextModel: "claude-sonnet-4-20250514", // Default model for chat
textModels: [
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 mini", provider: "openai" },
{ 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" },
],
},Environment Variables (Convex):
| Variable | Provider | Features |
|---|---|---|
ANTHROPIC_API_KEY |
Anthropic | Claude Sonnet 4 chat |
OPENAI_API_KEY |
OpenAI | GPT-4.1 mini chat |
GOOGLE_AI_API_KEY |
Gemini 2.0 Flash chat + image generation |
Optional system prompt variables:
CLAUDE_PROMPT_STYLE,CLAUDE_PROMPT_COMMUNITY,CLAUDE_PROMPT_RULES(optional): Split system promptsCLAUDE_SYSTEM_PROMPT(optional): Single system prompt fallback
Note: Only configure the API keys for providers you want to use. If a key is not set, users see a helpful setup message when they try to use that model.
Frontmatter Usage:
Enable AI chat on posts/pages:
---
title: My Post
rightSidebar: true
aiChat: true
---Requires rightSidebar: true and siteConfig.aiChat.enabledOnContent: true.
Dashboard AI Agent Features:
- Chat Tab: Multi-model selector with lazy API key validation
- Image Tab: AI image generation with aspect ratio selection (1:1, 16:9, 9:16, 4:3, 3:4)
- Images stored in Convex storage with session tracking
- Gallery view of recent generated images
Enable an Ask AI header chat button that opens a modal for asking questions about site content. Uses RAG (Retrieval Augmented Generation) with streaming responses.
{
"askAI": {
"enabled": true,
"defaultModel": "claude-sonnet-4-20250514",
"models": [
{ "id": "claude-sonnet-4-20250514", "name": "Claude Sonnet 4", "provider": "anthropic" },
{ "id": "gpt-4.1-mini", "name": "GPT-4.1 mini", "provider": "openai" }
]
}
}In src/config/siteConfig.ts:
askAI: {
enabled: true, // Enable Ask AI header button
defaultModel: "claude-sonnet-4-20250514",
models: [
{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", provider: "anthropic" },
{ id: "gpt-4.1-mini", name: "GPT-4.1 mini", provider: "openai" },
],
},Requirements:
semanticSearch.enabled: truefor content retrievalOPENAI_API_KEYin Convex for embeddingsANTHROPIC_API_KEYorOPENAI_API_KEYfor the LLM (depending on selected model)
Features:
- Header button opens a chat modal
- Retrieves relevant content using semantic search
- Streaming responses with markdown rendering
- Multi-model selector (Claude Sonnet 4, GPT-4.1 mini)
- Conversation history within session
Control where posts appear and limit homepage display.
{
"postsDisplay": {
"showOnHome": true,
"showOnBlogPage": true,
"homePostsLimit": 5,
"homePostsReadMore": {
"enabled": true,
"text": "Read more blog posts",
"link": "/blog"
}
}
}In src/config/siteConfig.ts:
postsDisplay: {
showOnHome: true, // Show post list on homepage
showOnBlogPage: true, // Show post list on /blog page
homePostsLimit: 5, // Limit posts on homepage (undefined = show all)
homePostsReadMore: {
enabled: true, // Show "read more" link when limited
text: "Read more blog posts",
link: "/blog",
},
},Copy this prompt to have an AI agent apply all changes:
I just forked the markdown-site repo. Please update all configuration files with my site information:
Site Name: [YOUR SITE NAME]
Site Title/Tagline: [YOUR TAGLINE]
Site Description: [YOUR DESCRIPTION]
Site URL: https://[YOURSITE].example.com
GitHub Username: [YOURUSERNAME]
GitHub Repo: [YOUR-REPO]
Contact Email: [your@email.com]
Creator Info:
- Name: [YOUR NAME]
- Twitter: https://x.com/[YOURHANDLE]
- LinkedIn: https://www.linkedin.com/in/[YOURPROFILE]/
- GitHub: https://github.com/[YOURUSERNAME]
GitHub Repo Config (for AI service links):
- Owner: [YOURUSERNAME]
- Repo: [YOUR-REPO]
- Branch: main
- Content Path: public/raw
Update these files:
1. src/config/siteConfig.ts - site name, bio, GitHub username, gitHubRepo config, defaultTheme
2. src/pages/Home.tsx - intro paragraph and footer section with all creator links
3. src/pages/Post.tsx - SITE_URL and SITE_NAME constants
4. src/pages/DocsPage.tsx - SITE_URL constant
5. convex/http.ts - SITE_URL and SITE_NAME constants
6. convex/rss.ts - SITE_URL, SITE_TITLE, SITE_DESCRIPTION
7. netlify/edge-functions/mcp.ts - SITE_URL, SITE_NAME, MCP_SERVER_NAME constants
8. scripts/send-newsletter.ts - default SITE_URL constant
9. index.html - all meta tags, JSON-LD, title
10. public/llms.txt - site info and GitHub link
11. public/robots.txt - header comment and sitemap URL
12. public/openapi.yaml - API title, server URL, contact URL, example URLs
13. public/.well-known/ai-plugin.json - plugin metadata and contact email
- Run
npx convex devto initialize Convex - Run
npm run syncto sync content to development - Run
npm run devto test locally - Deploy with Convex self-hosting when ready (
npm run deploy)
Note: Keep your fork-config.json file. When you run npm run sync:discovery or npm run sync:all (which includes content, wiki, and discovery sync), it reads from fork-config.json to update discovery files with your site information.
Discovery files (AGENTS.md, CLAUDE.md, and public/llms.txt) can be automatically updated with your current app data.
How it works: The sync:discovery script reads from fork-config.json (if it exists) to get your site name, URL, and GitHub info. This ensures your configured values are preserved when updating discovery files.
| Command | Description |
|---|---|
npm run sync:discovery |
Update discovery files with local Convex data |
npm run sync:discovery:prod |
Update discovery files with production Convex data |
npm run sync:wiki |
Sync wiki from content/blog and content/pages (dev) |
npm run sync:wiki:prod |
Sync wiki to production |
npm run sync:wiki -- --kb=<id> |
Sync wiki into a specific knowledge base |
npm run sync:all |
Sync content + wiki + discovery files (development) |
npm run sync:all:prod |
Sync content + wiki + discovery files (production) |
npm run sync: Run when you add, edit, or remove markdown contentnpm run sync:discovery: Run when you change site configuration or want to update discovery files with latest post countsnpm run sync:wiki: Run when you want to rebuild the wiki from content/blog and content/pagesnpm run sync:all: Run all syncs together (recommended for complete updates)
| File | Updated Content |
|---|---|
AGENTS.md |
Project overview, current status (site name, URL, post/page counts) |
public/llms.txt |
Site info, total posts, latest post date, GitHub URL |
The script reads from siteConfig.ts and queries Convex for live content statistics.
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) |
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 |
The site serves pre-rendered HTML with correct canonical URLs and meta tags to search engines and social preview bots. Configure bot detection in netlify/edge-functions/botMeta.ts.
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 |
Edit the arrays at the top of netlify/edge-functions/botMeta.ts:
// Add or remove social preview bots
const SOCIAL_PREVIEW_BOTS = [
"facebookexternalhit",
"twitterbot",
// ... add your own
];
// Add or remove search engine bots
const SEARCH_ENGINE_BOTS = [
"googlebot",
"bingbot",
// ... add your own
];
// Add or remove AI crawlers
const AI_CRAWLERS = [
"gptbot",
"claudebot",
// ... add your own
];Test with curl to simulate different bots:
# Test Googlebot (should get pre-rendered HTML with correct canonical)
curl -H "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1)" \
https://yoursite.com/your-post | grep canonical
# Test normal browser (should get SPA with homepage canonical)
curl https://yoursite.com/your-post | grep canonicalSingle-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.
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.
- Navigate to
/dashboard - Go to the Config section
- Find the Version Control card
- Toggle Enable version control on
- 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)
| Source | When created |
|---|---|
| sync | Before markdown sync updates (npm run sync) |
| dashboard | Before saving edits in Dashboard |
| restore | Before restoring a previous version |
- Open any post or page in the Dashboard editor
- Click the History button (clock icon) in the editor toolbar
- Select a version from the list
- View diff or preview
- Click Restore This Version to revert
- Versions stored in
contentVersionstable in Convex - Settings stored in
versionControlSettingstable - Cleanup via cron job in
convex/crons.ts - Version capture is async (non-blocking via
ctx.scheduler.runAfter)
Version control settings are stored in the Convex database rather than config files because:
- Toggle requires real-time state - UI needs to reflect current setting immediately
- Shared across environments - Same setting for all users of the dashboard
- No redeploy needed - Toggle works instantly without rebuilding