This guide walks you through forking [this markdown framework](https://github.com/waynesutton/markdown-site), 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.
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.xml` and `/rss-full.xml`
-`sitemap.ts` - Proxies `/sitemap.xml`
-`api.ts` - Proxies `/api/posts` and `/api/post`
-`botMeta.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](https://docs.convex.dev/production/hosting/netlify).
### Option A: Netlify CLI
```bash
# 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
1. Go to [app.netlify.com](https://app.netlify.com)
2. Click "Add new site" then "wImport an existing project"
3. Connect your GitHub repository
4. Configure build settings:
- Build command: `npm ci --include=dev && npx convex deploy --cmd 'npm run build'`
-`VITE_CONVEX_URL`: Your production Convex URL (e.g., `https://your-deployment.convex.cloud`)
6. 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:
```toml
[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=dev` forces devDependencies to install even when `NODE_ENV=production`
- The build script uses `npx vite build` to resolve vite from node_modules
-`@types/node` is required for TypeScript to recognize `process.env`
## Step 8: Set Up Production Convex
For production, deploy your Convex functions:
```bash
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/`:
```markdown
---
title: "Your Post Title"
description: "A brief description for SEO and social sharing"
The `npm run sync` command only syncs markdown text content. Images are deployed when Netlify builds your site. Use `npm run sync:discovery` to update discovery files (AGENTS.md, llms.txt) when site configuration changes.
Get your production URL from the [Convex Dashboard](https://dashboard.convex.dev) by selecting your project and switching to the Production deployment.
**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`.
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.
**Inner page logo:** Shows on blog page, individual posts, and static pages. Configure in `src/config/siteConfig.ts`:
```typescript
innerPageLogo: {
enabled: true, // Set to false to hide logo on inner pages
size: 28, // Logo height in pixels (keeps aspect ratio)
},
```
The inner page logo appears in the top left corner on desktop and top right on mobile. It uses the same logo file as the homepage logo. Set `enabled: false` to hide it on inner pages while keeping the homepage logo.
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.
| `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.
| `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).
Display real-time visitor locations on a world map on the stats page. Uses Netlify's built-in geo detection (no third-party API needed). Privacy friendly: only stores city, country, and coordinates. No IP addresses stored.
Configure in `siteConfig`:
```typescript
visitorMap: {
enabled: true, // Set to false to hide the visitor map
title: "Live Visitors", // Optional title above the map
| `title` | Text above map (set to `undefined` to hide) |
The map displays with theme-aware colors. Visitor dots pulse to indicate live sessions. Location data comes from Netlify's automatic geo headers at the edge.
The site supports a dedicated blog page at `/blog` with two view modes: list view (year-grouped posts) and card view (thumbnail grid). Configure in `src/config/siteConfig.ts`:
- **List view:** Year-grouped posts with titles, read time, and dates
- **Card view:** Grid of cards showing thumbnails, titles, excerpts, and metadata
**Card view details:**
Cards display post thumbnails (from `image` frontmatter field), titles, excerpts (or descriptions), read time, and dates. Posts without images show cards without thumbnail areas. Grid is responsive: 3 columns on desktop, 2 on tablet, 1 on mobile.
**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.
Posts can be marked as featured on the blog page using the `blogFeatured` frontmatter field:
```yaml
---
title: "My Featured Post"
blogFeatured: true
---
```
The first `blogFeatured` post displays as a hero card with landscape image, tags, date, title, excerpt, author info, and read more link. Remaining `blogFeatured` posts display in a 2-column featured row with excerpts. Regular (non-featured) posts display in a 3-column grid without excerpts.
### Homepage Post Limit
Limit the number of posts shown on the homepage:
```typescript
postsDisplay: {
showOnHome: true,
homePostsLimit: 5, // Limit to 5 most recent posts (undefined = show all)
homePostsReadMore: {
enabled: true,
text: "Read more blog posts",
link: "/blog",
},
},
```
When posts are limited, an optional "read more" link appears below the list. Only shows when there are more posts than the limit.
Add React route pages (like `/stats`, `/write`) to the navigation menu via `siteConfig.ts`. These pages are React components, not markdown files.
Configure in `src/config/siteConfig.ts`:
```typescript
hardcodedNavItems: [
{
slug: "stats",
title: "Stats",
order: 10,
showInNav: true, // Set to false to hide from nav
},
{
slug: "write",
title: "Write",
order: 20,
showInNav: true,
},
],
```
Navigation combines three sources in this order:
1. Blog link (if `blogPage.enabled` and `blogPage.showInNav` are true)
2. Hardcoded nav items (from `hardcodedNavItems` array)
3. Markdown pages (from `content/pages/` with `showInNav: true`)
All items sort by `order` field (lower numbers first), then alphabetically by title.
**Hide from navigation:** Set `showInNav: false` to keep a route accessible but hidden from the nav menu. The route still works at its URL, just won't appear in navigation links.
**Hide pages from navigation:** Set `showInNav: false` in page frontmatter to keep a page published and accessible via direct URL, but hidden from the navigation menu. Useful for pages like `/projects` that you want to link directly but not show in the main nav. Pages with `showInNav: false` remain searchable and available via API endpoints.
**Sidebar layout:** Add `layout: "sidebar"` to any post or page frontmatter to enable a docs-style layout with a table of contents sidebar. The sidebar extracts headings (H1, H2, H3) automatically and provides smooth scroll navigation. Only appears if headings exist in the content.
**Right sidebar:** When enabled in `siteConfig.rightSidebar.enabled`, posts and pages can display a right sidebar containing the CopyPageDropdown at 1135px+ viewport width. Add `rightSidebar: true` to frontmatter to enable. Without this field, pages render normally with CopyPageDropdown in the nav bar. When enabled, CopyPageDropdown moves from the navigation bar to the right sidebar on wide screens. The right sidebar is hidden below 1135px, and CopyPageDropdown returns to the nav bar automatically.
**Show image at top:** Add `showImageAtTop: true` to display the `image` field at the top of the post/page above the header. Default behavior: if `showImageAtTop` is not set or `false`, image only used for Open Graph previews and featured card thumbnails.
**Footer:** Footer content can be set in frontmatter (`footer` field) or use `siteConfig.footer.defaultContent`. Control visibility globally via `siteConfig.footer.enabled` and per-page via `showFooter: true/false` frontmatter.
**Social footer:** Display social icons and copyright below the main footer. Configure via `siteConfig.socialFooter`. Control visibility per-page via `showSocialFooter: true/false` frontmatter.
**Contact form:** Enable contact forms on any page or post via `contactForm: true` frontmatter. Requires `AGENTMAIL_API_KEY` and `AGENTMAIL_INBOX` environment variables in Convex. See the [Contact Form section](#contact-form-configuration) below.
**AI Agent chat:** The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Enable Agent on the Write page via `siteConfig.aiChat.enabledOnWritePage` or in the right sidebar on posts/pages using `aiChat: true` frontmatter (requires `rightSidebar: true`). Requires `ANTHROPIC_API_KEY` environment variable in Convex. See the [AI Agent chat section](#ai-agent-chat) below for setup instructions.
Tag pages are available at `/tags/[tag]` for each tag used in your posts. They display all posts with that tag in a list or card view.
**Related posts:** Individual blog posts show up to 3 related posts in the footer based on shared tags. Posts are sorted by relevance (number of shared tags) then by date. Only appears on blog posts (not static pages).
**Tag links:** Tags in post footers link to their respective tag archive pages.
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)
| `AGENTMAIL_INBOX` | Your AgentMail inbox address |
| `AGENTMAIL_CONTACT_EMAIL` | Optional recipient for contact forms |
**Important:** If environment variables are not configured, users will see an error message when attempting to use newsletter or contact form features: "AgentMail Environment Variables are not configured in production. Please set AGENTMAIL_API_KEY and AGENTMAIL_INBOX."
**Sending newsletters:**
Two modes are available:
1.**Send Post**: Select a blog post to send to all active subscribers
2.**Write Email**: Compose custom content with markdown support
The admin UI shows send results and provides CLI commands as alternatives.
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:
**Git push required for AI links:** The "Open in ChatGPT," "Open in Claude," and "Open in Perplexity" options use GitHub raw URLs to fetch content. For these to work, your content must be pushed to GitHub with `git push`. The `npm run sync` command syncs content to Convex for your live site, but AI services fetch directly from GitHub.
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:**
1. Go to `yourdomain.com/write`
2. Select content type (Blog Post or Page)
3. Write your content using the frontmatter reference
4. Click "Copy All" to copy the markdown
5. Save to `content/blog/` or `content/pages/`
6. Run `npm run sync` or `npm 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.
**AI Agent mode:** When `siteConfig.aiChat.enabledOnWritePage` is enabled, a toggle button appears in the Actions section. Clicking it replaces the textarea with the AI Agent chat interface. The page title changes to "Agent" when in chat mode. Requires `ANTHROPIC_API_KEY` environment variable in Convex. See the [AI Agent chat section](#ai-agent-chat) below for setup instructions.
## AI Agent chat
The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled in two places:
**1. Write page (`/write`)**
Enable Agent mode on the Write page via `siteConfig.aiChat.enabledOnWritePage`. When enabled, a toggle button appears in the Actions section. Clicking it replaces the textarea with the Agent chat interface. The page title changes to "Agent" when in chat mode.
**Configuration:**
```typescript
// src/config/siteConfig.ts
aiChat: {
enabledOnWritePage: true, // Enable Agent toggle on /write page
enabledOnContent: true, // Allow Agent on posts/pages via frontmatter
},
```
**2. Right sidebar on posts/pages**
Enable Agent in the right sidebar on individual posts or pages using the `aiChat` frontmatter field. Requires both `rightSidebar: true` and `siteConfig.aiChat.enabledOnContent: true`.
Agent requires the following Convex environment variables:
-`ANTHROPIC_API_KEY` (required): Your Anthropic API key for Claude API access
-`CLAUDE_PROMPT_STYLE` (optional): First part of system prompt
-`CLAUDE_PROMPT_COMMUNITY` (optional): Second part of system prompt
-`CLAUDE_PROMPT_RULES` (optional): Third part of system prompt
-`CLAUDE_SYSTEM_PROMPT` (optional): Single system prompt (fallback if split prompts not set)
**Setting environment variables:**
1. Go to [Convex Dashboard](https://dashboard.convex.dev)
2. Select your project
3. Navigate to Settings > Environment Variables
4. Add `ANTHROPIC_API_KEY` with your API key value
5. Optionally add system prompt variables (`CLAUDE_PROMPT_STYLE`, etc.)
6. Deploy changes
**How it works:**
- Agent uses anonymous session IDs stored in localStorage for chat history
- Each post/page has its own chat context (identified by slug)
- Chat history is stored per-session, per-context in Convex (aiChats table)
- Page content can be provided as context for AI responses
- Chat history limited to last 20 messages for efficiency
- If API key is not set, Agent displays "API key is not set" error message
**Error handling:**
If `ANTHROPIC_API_KEY` is not configured in Convex environment variables, Agent displays a user-friendly error message: "API key is not set". This helps identify when the API key is missing in production deployments.
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.