# Markdown Site A minimalist markdown site built with React, Convex, and Vite. Optimized for SEO, AI agents, and LLM discovery. **How publishing works:** 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. ## Features - Markdown-based blog posts with frontmatter - Syntax highlighting for code blocks - Four theme options: Dark, Light, Tan (default), Cloud - Real-time data with Convex - Fully responsive design ### SEO and Discovery - RSS feeds at `/rss.xml` and `/rss-full.xml` (with full content) - Dynamic sitemap at `/sitemap.xml` - JSON-LD structured data for Google rich results - Open Graph and Twitter Card meta tags - `robots.txt` with AI crawler rules - `llms.txt` for AI agent discovery ### AI and LLM Access - `/api/posts` - JSON list of all posts for agents - `/api/post?slug=xxx` - Single post JSON or markdown - `/rss-full.xml` - Full content RSS for LLM ingestion - Copy Page dropdown for sharing to ChatGPT, Claude ## Getting Started ### Prerequisites - Node.js 18 or higher - A Convex account ### Setup 1. Install dependencies: ```bash npm install ``` 2. Initialize Convex: ```bash npx convex dev ``` This will create your Convex project and generate the `.env.local` file. 3. Start the development server: ```bash npm run dev ``` 4. Open http://localhost:5173 ## Writing Blog Posts Create markdown files in `content/blog/` with frontmatter: ## Static Pages (Optional) Create optional pages like About, Projects, or Contact in `content/pages/`: ```markdown --- title: "About" slug: "about" published: true order: 1 --- Your page content here... ``` Pages appear as navigation links in the top right, next to the theme toggle. The `order` field controls display order (lower numbers first). ```markdown --- title: "Your Post Title" description: "A brief description" date: "2025-01-15" slug: "your-post-slug" published: true tags: ["tag1", "tag2"] readTime: "5 min read" image: "/images/my-header.png" --- Your markdown content here... ``` ## Images ### Open Graph Images Add an `image` field to frontmatter for social media previews: ```yaml image: "/images/my-header.png" ``` Recommended dimensions: 1200x630 pixels. Images can be local (`/images/...`) or external URLs. ### Inline Images Add images in markdown content: ```markdown ![Alt text description](/images/screenshot.png) ``` Place image files in `public/images/`. The alt text displays as a caption. ### Site Logo Edit `src/pages/Home.tsx` to set your site logo: ```typescript const siteConfig = { logo: "/images/logo.svg", // Set to null to hide // ... }; ``` Replace `public/images/logo.svg` with your own logo file. ### Favicon Replace `public/favicon.svg` with your own icon. The default is a rounded square with the letter "m". Edit the SVG to change the letter or style. ### Default Open Graph Image The default OG image is used when posts do not have an `image` field. Replace `public/images/og-default.svg` with your own image (1200x630 recommended). Update the reference in `src/pages/Post.tsx`: ```typescript const DEFAULT_OG_IMAGE = "/images/og-default.svg"; ``` ## Syncing Posts Posts are synced to Convex. The sync script reads markdown files from `content/blog/` and `content/pages/`, then uploads them to your Convex database. ### Environment Files | File | Purpose | | ----------------------- | -------------------------------------------------------- | | `.env.local` | Development deployment URL (created by `npx convex dev`) | | `.env.production.local` | Production deployment URL (create manually) | Both files are gitignored. Each developer creates their own. ### Sync Commands | Command | Target | When to use | | ------------------- | ----------- | --------------------------- | | `npm run sync` | Development | Local testing, new posts | | `npm run sync:prod` | Production | Deploy content to live site | **Development sync:** ```bash npm run sync ``` **Production sync:** First, create `.env.production.local` with your production Convex URL: ``` VITE_CONVEX_URL=https://your-prod-deployment.convex.cloud ``` Then sync: ```bash npm run sync:prod ``` ## Deployment ### Netlify [![Netlify Status](https://api.netlify.com/api/v1/badges/d8c4d83d-7486-42de-844b-6f09986dc9aa/deploy-status)](https://app.netlify.com/projects/markdowncms/deploys) For detailed setup, see the [Convex Netlify Deployment Guide](https://docs.convex.dev/production/hosting/netlify). 1. Deploy Convex functions to production: ```bash npx convex deploy ``` Note the production URL (e.g., `https://your-deployment.convex.cloud`). 2. Connect your repository to Netlify 3. Configure build settings: - Build command: `npm ci --include=dev && npx convex deploy --cmd 'npm run build'` - Publish directory: `dist` 4. Add environment variables in Netlify dashboard: - `CONVEX_DEPLOY_KEY` - Generate from [Convex Dashboard](https://dashboard.convex.dev) > Project Settings > Deploy Key - `VITE_CONVEX_URL` - Your production Convex URL (e.g., `https://your-deployment.convex.cloud`) The `CONVEX_DEPLOY_KEY` deploys functions at build time. The `VITE_CONVEX_URL` is required for edge functions (RSS, sitemap, API) to proxy requests at runtime. **Build issues?** Netlify sets `NODE_ENV=production` which skips devDependencies. The `--include=dev` flag fixes this. See [netlify-deploy-fix.md](./netlify-deploy-fix.md) for detailed troubleshooting. ## Project Structure ``` markdown-site/ ├── content/blog/ # Markdown blog posts ├── convex/ # Convex backend │ ├── http.ts # HTTP endpoints (sitemap, API, RSS) │ ├── posts.ts # Post queries and mutations │ ├── rss.ts # RSS feed generation │ └── schema.ts # Database schema ├── netlify/ # Netlify edge functions │ └── edge-functions/ │ ├── rss.ts # RSS feed proxy │ ├── sitemap.ts # Sitemap proxy │ ├── api.ts # API endpoint proxy │ └── botMeta.ts # OG crawler detection ├── public/ # Static assets │ ├── images/ # Blog images and OG images │ ├── robots.txt # Crawler rules │ └── llms.txt # AI agent discovery ├── scripts/ # Build scripts └── src/ ├── components/ # React components ├── context/ # Theme context ├── pages/ # Page components └── styles/ # Global CSS ``` ## Scripts Reference | Script | Description | | --------------------- | -------------------------------------------- | | `npm run dev` | Start Vite dev server | | `npm run dev:convex` | Start Convex dev backend | | `npm run sync` | Sync posts to dev deployment | | `npm run sync:prod` | Sync posts to production deployment | | `npm run build` | Build for production | | `npm run deploy` | Sync + build (for manual deploys) | | `npm run deploy:prod` | Deploy Convex functions + sync to production | ## Tech Stack - React 18 - TypeScript - Vite - Convex - react-markdown - react-syntax-highlighter - date-fns - lucide-react - Netlify ## API Endpoints | Endpoint | Description | | ------------------------------ | ------------------------------- | | `/rss.xml` | RSS feed with post descriptions | | `/rss-full.xml` | RSS feed with full post 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 markdown | | `/meta/post?slug=xxx` | Open Graph HTML for crawlers | ## How Blog Post Slugs Work Slugs are defined in the frontmatter of each markdown file: ```markdown --- slug: "my-post-slug" --- ``` The slug becomes the URL path: `yourdomain.com/my-post-slug` Rules: - Slugs must be unique across all posts - Use lowercase letters, numbers, and hyphens - The sync script reads the `slug` field from frontmatter - Posts are queried by slug using a Convex index ## Theme Configuration The default theme is Tan. Users can cycle through themes using the toggle: - Dark (Moon icon) - Light (Sun icon) - Tan (Half icon) - default - Cloud (Cloud icon) To change the default theme, edit `src/context/ThemeContext.tsx`: ```typescript const DEFAULT_THEME: Theme = "tan"; // Change to "dark", "light", or "cloud" ``` ## Font Configuration The blog uses a serif font (New York) by default. To switch fonts, edit `src/styles/global.css`: ```css body { /* Sans-serif option */ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; /* Serif option (default) */ font-family: "New York", -apple-system-ui-serif, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; } ``` Replace the `font-family` property with your preferred font stack. ## Source Fork this project: [github.com/waynesutton/markdown-site](https://github.com/waynesutton/markdown-site)