mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
feat: Add semantic search with vector embeddings
Add vector-based semantic search to complement keyword search. Users can toggle between "Keyword" and "Semantic" modes in the search modal (Cmd+K, then Tab to switch). Semantic search: - Uses OpenAI text-embedding-ada-002 (1536 dimensions) - Finds content by meaning, not exact words - Shows similarity scores as percentages - ~300ms latency, ~$0.0001/query - Graceful fallback if OPENAI_API_KEY not set New files: - convex/embeddings.ts - Embedding generation actions - convex/embeddingsQueries.ts - Queries/mutations for embeddings - convex/semanticSearch.ts - Vector search action - convex/semanticSearchQueries.ts - Result hydration queries - content/pages/docs-search.md - Keyword search docs - content/pages/docs-semantic-search.md - Semantic search docs Changes: - convex/schema.ts: Add embedding field and by_embedding vectorIndex - SearchModal.tsx: Add mode toggle (TextAa/Brain icons) - sync-posts.ts: Generate embeddings after content sync - global.css: Search mode toggle styles Documentation updated: - changelog.md, TASK.md, files.md, about.md, home.md Configuration: npx convex env set OPENAI_API_KEY sk-your-key Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Status: Ready to commit. All semantic search files are staged. The TypeScript warnings are pre-existing (unused variables) and don't affect the build.
This commit is contained in:
@@ -1,17 +1,32 @@
|
||||
---
|
||||
title: "Netlify edge functions blocking AI crawlers from static files"
|
||||
description: "Why excludedPath in netlify.toml isn't preventing edge functions from intercepting /raw/* requests, and how ChatGPT and Perplexity get blocked while Claude works."
|
||||
title: "How we fixed AI crawlers blocked by Netlify edge functions"
|
||||
description: "ChatGPT and Perplexity couldn't fetch /raw/*.md files on Netlify. The fix: Content-Type headers. Here's what we tried and what actually worked."
|
||||
date: "2025-12-14"
|
||||
slug: "netlify-edge-excludedpath-ai-crawlers"
|
||||
published: true
|
||||
tags: ["netlify", "edge-functions", "ai", "troubleshooting", "help"]
|
||||
tags: ["netlify", "edge-functions", "ai", "troubleshooting"]
|
||||
readTime: "5 min read"
|
||||
featured: false
|
||||
---
|
||||
|
||||
## The fix
|
||||
|
||||
Add explicit `Content-Type` headers for your raw markdown files in `netlify.toml`:
|
||||
|
||||
```toml
|
||||
[[headers]]
|
||||
for = "/raw/*"
|
||||
[headers.values]
|
||||
Content-Type = "text/plain; charset=utf-8"
|
||||
Access-Control-Allow-Origin = "*"
|
||||
Cache-Control = "public, max-age=3600"
|
||||
```
|
||||
|
||||
Thanks to [KP](https://x.com/thisiskp_) for pointing us in the right direction.
|
||||
|
||||
## The problem
|
||||
|
||||
AI crawlers cannot access static markdown files at `/raw/*.md` on Netlify, even with `excludedPath` configured. ChatGPT and Perplexity return errors. Claude works.
|
||||
AI crawlers could not access static markdown files at `/raw/*.md` on Netlify, even with `excludedPath` configured. ChatGPT and Perplexity returned errors. Claude worked.
|
||||
|
||||
## What we're building
|
||||
|
||||
@@ -164,15 +179,19 @@ The core issue appears to be how ChatGPT and Perplexity fetch URLs. Their tools
|
||||
2. The edge function exclusions work for browsers but not for AI fetch tools
|
||||
3. There may be rate limiting or bot protection enabled by default
|
||||
|
||||
## Current workaround
|
||||
## Why Content-Type matters
|
||||
|
||||
Users can still share content with AI tools by:
|
||||
Without an explicit `Content-Type` header, Netlify serves files based on extension. The `.md` extension gets served as `text/markdown` or similar, which AI fetch tools may reject or misinterpret.
|
||||
|
||||
1. **Copy page** copies markdown to clipboard, then paste into any AI
|
||||
2. **View as Markdown** opens the raw `.md` file in a browser tab for manual copying
|
||||
3. **Download as SKILL.md** downloads in Anthropic Agent Skills format
|
||||
Setting `Content-Type = "text/plain; charset=utf-8"` tells the CDN and AI crawlers exactly what to expect. The `Access-Control-Allow-Origin = "*"` header ensures cross-origin requests work.
|
||||
|
||||
The direct "Open in ChatGPT/Claude/Perplexity" buttons have been disabled since the URLs don't work reliably.
|
||||
## What works now
|
||||
|
||||
Users can share content with AI tools via:
|
||||
|
||||
1. **Copy page** copies markdown to clipboard
|
||||
2. **View as Markdown** opens the raw `.md` file in browser
|
||||
3. **Open in ChatGPT/Claude/Perplexity** sends the URL directly (now working)
|
||||
|
||||
## Working features
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Setup Guide - Fork and Deploy Your Own Markdown Framework"
|
||||
title: "Setup Guide"
|
||||
description: "Step-by-step guide to fork this markdown sync framework, set up Convex backend, and deploy to Netlify in under 10 minutes."
|
||||
date: "2025-12-14"
|
||||
slug: "setup-guide"
|
||||
@@ -348,29 +348,29 @@ 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) |
|
||||
| `authorName` | No | Author display name shown next to date |
|
||||
| `authorImage` | No | Round author avatar image URL |
|
||||
| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) |
|
||||
| `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. |
|
||||
| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. |
|
||||
| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. |
|
||||
| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. |
|
||||
| `docsSectionGroupIcon` | No | Phosphor icon name for docs sidebar group (e.g., "Rocket", "Book", "PuzzlePiece"). Icon appears left of the group title. See [Phosphor Icons](https://phosphoricons.com) for available icons. |
|
||||
| `docsLanding` | No | Set `true` to use as the docs landing page (shown when navigating to `/docs`). |
|
||||
| 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) |
|
||||
| `authorName` | No | Author display name shown next to date |
|
||||
| `authorImage` | No | Round author avatar image URL |
|
||||
| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) |
|
||||
| `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. |
|
||||
| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. |
|
||||
| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. |
|
||||
| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. |
|
||||
| `docsSectionGroupIcon` | No | Phosphor icon name for docs sidebar group (e.g., "Rocket", "Book", "PuzzlePiece"). Icon appears left of the group title. See [Phosphor Icons](https://phosphoricons.com) for available icons. |
|
||||
| `docsLanding` | No | Set `true` to use as the docs landing page (shown when navigating to `/docs`). |
|
||||
|
||||
### How Frontmatter Works
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: "Team Workflows with Git Version Control"
|
||||
title: "Team Workflows"
|
||||
description: "How teams collaborate on markdown content using git, sync to shared Convex deployments, and automate production syncs with CI/CD."
|
||||
date: "2025-12-29"
|
||||
slug: "team-workflows-git-version-control"
|
||||
|
||||
@@ -83,7 +83,9 @@ It's a hybrid: developer workflow for publishing + real-time delivery like a dyn
|
||||
|
||||
**Search and discovery:**
|
||||
|
||||
- Full text search with Command+K shortcut
|
||||
- Dual search modes: Keyword (exact match) and Semantic (meaning-based) with Cmd+K toggle
|
||||
- Semantic search uses OpenAI embeddings for finding conceptually similar content
|
||||
- Full text search with Command+K shortcut and result highlighting
|
||||
- Static raw markdown files at `/raw/{slug}.md`
|
||||
- RSS feeds (`/rss.xml` and `/rss-full.xml`) and sitemap for SEO
|
||||
- API endpoints for AI/LLM access (`/api/posts`, `/api/export`)
|
||||
|
||||
@@ -11,6 +11,117 @@ docsSectionOrder: 4
|
||||
|
||||
All notable changes to this project.
|
||||
|
||||
## v2.10.0
|
||||
|
||||
Released January 5, 2026
|
||||
|
||||
**Semantic search with vector embeddings**
|
||||
|
||||
Search now supports two modes accessible via Cmd+K:
|
||||
|
||||
- **Keyword search** (existing) - Matches exact words using Convex full-text search. Instant, free, supports highlighting.
|
||||
- **Semantic search** (new) - Finds content by meaning using OpenAI embeddings. Toggle to "Semantic" mode in search modal.
|
||||
|
||||
**How semantic search works:**
|
||||
|
||||
1. Your query is converted to a 1536-dimension vector using OpenAI text-embedding-ada-002
|
||||
2. Convex compares this vector to stored embeddings for all posts and pages
|
||||
3. Results ranked by similarity score (displayed as percentage)
|
||||
4. Top 15 results returned
|
||||
|
||||
**When to use each mode:**
|
||||
|
||||
| Use Case | Mode |
|
||||
|----------|------|
|
||||
| Specific code, commands, exact phrases | Keyword |
|
||||
| Conceptual questions ("how do I deploy?") | Semantic |
|
||||
| Need to highlight matches on page | Keyword |
|
||||
| Not sure of exact terminology | Semantic |
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Semantic search requires an OpenAI API key:
|
||||
|
||||
```bash
|
||||
npx convex env set OPENAI_API_KEY sk-your-key-here
|
||||
npm run sync # Generates embeddings for all content
|
||||
```
|
||||
|
||||
If OPENAI_API_KEY is not configured, semantic search returns empty results and keyword search continues to work normally.
|
||||
|
||||
**Technical details:**
|
||||
|
||||
- New files: `convex/embeddings.ts`, `convex/embeddingsQueries.ts`, `convex/semanticSearch.ts`, `convex/semanticSearchQueries.ts`
|
||||
- Added `embedding` field and `by_embedding` vector index to posts and pages tables
|
||||
- SearchModal.tsx updated with Keyword/Semantic toggle (TextAa and Brain icons)
|
||||
- Embeddings generated automatically during `npm run sync`
|
||||
- Cost: ~$0.0001 per search query (embedding generation)
|
||||
|
||||
Updated files: `convex/schema.ts`, `convex/embeddings.ts`, `convex/embeddingsQueries.ts`, `convex/semanticSearch.ts`, `convex/semanticSearchQueries.ts`, `src/components/SearchModal.tsx`, `scripts/sync-posts.ts`, `src/styles/global.css`, `content/pages/docs-search.md`, `content/pages/docs-semantic-search.md`
|
||||
|
||||
## v2.9.0
|
||||
|
||||
Released January 4, 2026
|
||||
|
||||
**Dashboard Cloud CMS Features**
|
||||
|
||||
The Dashboard now functions as a WordPress-style cloud CMS, allowing content creation and editing directly in the database without requiring the markdown file sync workflow.
|
||||
|
||||
**Dual Source Architecture:**
|
||||
|
||||
- Dashboard-created content marked with `source: "dashboard"`
|
||||
- Markdown-synced content marked with `source: "sync"`
|
||||
- Both coexist independently in the database
|
||||
- Sync operations only affect synced content (dashboard content protected)
|
||||
- Source badges in Posts and Pages list views (blue "Dashboard", gray "Synced")
|
||||
|
||||
**Direct Database Operations:**
|
||||
|
||||
- "Save to DB" button in Write Post/Page sections saves directly to database
|
||||
- "Save Changes" button in Post/Page editor updates content immediately
|
||||
- Delete button for dashboard-created content (synced content protected)
|
||||
- Changes appear instantly without requiring sync
|
||||
|
||||
**Delete Confirmation Modal:**
|
||||
|
||||
- Warning modal displayed before deleting posts or pages
|
||||
- Shows item name and type being deleted
|
||||
- Themed to match dashboard UI with danger button styling
|
||||
- Backdrop click and Escape key to cancel
|
||||
|
||||
**Rich Text Editor:**
|
||||
|
||||
- Three editing modes: Markdown (default), Rich Text (Quill WYSIWYG), Preview
|
||||
- Quill-based editor with formatting toolbar
|
||||
- Toolbar: headers (H1-H3), bold, italic, strikethrough, blockquote, code, lists, links
|
||||
- Automatic HTML-to-Markdown conversion when switching modes
|
||||
- Theme-aware styling using CSS variables
|
||||
|
||||
**Server-Side URL Import:**
|
||||
|
||||
- Direct database import via Firecrawl (no file sync needed)
|
||||
- Enter URL in Import section, content is scraped and saved to database
|
||||
- Optional "Publish immediately" checkbox
|
||||
- Imported posts tagged with `imported` by default
|
||||
- Requires `FIRECRAWL_API_KEY` in Convex environment variables
|
||||
|
||||
**Export to Markdown:**
|
||||
|
||||
- Export any post/page to `.md` file with complete frontmatter
|
||||
- Bulk export script: `npm run export:db` (dev) or `npm run export:db:prod` (prod)
|
||||
- Use for backup or converting dashboard content to file-based workflow
|
||||
|
||||
**Technical details:**
|
||||
|
||||
- New file: `convex/cms.ts` with CRUD mutations
|
||||
- New file: `convex/importAction.ts` with Firecrawl server-side action
|
||||
- New file: `scripts/export-db-posts.ts` for bulk markdown export
|
||||
- Added `source` field and `by_source` index to posts and pages tables
|
||||
- Added ConfirmDeleteModal component to Dashboard.tsx
|
||||
- Fixed list row grid layout for proper source badge display
|
||||
|
||||
Updated files: `convex/schema.ts`, `convex/posts.ts`, `convex/pages.ts`, `convex/cms.ts`, `convex/importAction.ts`, `scripts/export-db-posts.ts`, `src/pages/Dashboard.tsx`, `src/styles/global.css`, `package.json`, `content/pages/docs-dashboard.md`
|
||||
|
||||
## v2.8.7
|
||||
|
||||
Released January 4, 2026
|
||||
|
||||
444
content/pages/docs-configuration.md
Normal file
444
content/pages/docs-configuration.md
Normal file
@@ -0,0 +1,444 @@
|
||||
---
|
||||
title: "Configuration"
|
||||
slug: "docs-configuration"
|
||||
published: true
|
||||
order: 4
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 4
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Fork configuration
|
||||
|
||||
After forking, you have two options to configure your site:
|
||||
|
||||
**Option 1: Automated (Recommended)**
|
||||
|
||||
```bash
|
||||
cp fork-config.json.example fork-config.json
|
||||
# Edit fork-config.json with your site information
|
||||
npm run configure
|
||||
```
|
||||
|
||||
This updates all 11 configuration files in one command. See `FORK_CONFIG.md` for the full JSON schema and options.
|
||||
|
||||
**Option 2: Manual**
|
||||
|
||||
Follow the step-by-step guide in `FORK_CONFIG.md` to update each file manually.
|
||||
|
||||
### Files updated by configuration
|
||||
|
||||
| File | What to update |
|
||||
| ----------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `src/config/siteConfig.ts` | Site name, title, intro, bio, blog page, logo gallery, GitHub contributions, right sidebar configuration |
|
||||
| `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/config/siteConfig.ts` | Default theme (`defaultTheme` field) |
|
||||
|
||||
### 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 |
|
||||
|
||||
**Backend constants** (`convex/http.ts` and `convex/rss.ts`):
|
||||
|
||||
```typescript
|
||||
// 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.";
|
||||
```
|
||||
|
||||
**Post page constants** (`src/pages/Post.tsx`):
|
||||
|
||||
```typescript
|
||||
const SITE_URL = "https://your-site.netlify.app";
|
||||
const SITE_NAME = "Your Site Name";
|
||||
const DEFAULT_OG_IMAGE = "/images/og-default.svg";
|
||||
```
|
||||
|
||||
These constants affect RSS feeds, API responses, sitemaps, and social sharing metadata.
|
||||
|
||||
### Site settings
|
||||
|
||||
Edit `src/config/siteConfig.ts`:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
name: "Site Name",
|
||||
title: "Tagline",
|
||||
logo: "/images/logo.svg", // null to hide homepage logo
|
||||
intro: "Introduction text...",
|
||||
bio: "Bio text...",
|
||||
|
||||
// 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)
|
||||
},
|
||||
|
||||
// Hardcoded navigation items for React routes
|
||||
hardcodedNavItems: [
|
||||
{
|
||||
slug: "stats",
|
||||
title: "Stats",
|
||||
order: 10,
|
||||
showInNav: true, // Set to false to hide from nav
|
||||
},
|
||||
{
|
||||
slug: "write",
|
||||
title: "Write",
|
||||
order: 20,
|
||||
showInNav: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Inner page logo configuration
|
||||
innerPageLogo: {
|
||||
enabled: true, // Set to false to hide logo on inner pages
|
||||
size: 28, // Logo height in pixels (keeps aspect ratio)
|
||||
},
|
||||
|
||||
// Featured section
|
||||
featuredViewMode: "list", // 'list' or 'cards'
|
||||
showViewToggle: true,
|
||||
|
||||
// Logo gallery (static grid or scrolling marquee)
|
||||
logoGallery: {
|
||||
enabled: true, // false to hide
|
||||
images: [{ src: "/images/logos/logo.svg", href: "https://example.com" }],
|
||||
position: "above-footer",
|
||||
speed: 30,
|
||||
title: "Built with",
|
||||
scrolling: false, // false = static grid, true = scrolling marquee
|
||||
maxItems: 4, // Number of logos when scrolling is false
|
||||
},
|
||||
|
||||
links: {
|
||||
docs: "/docs",
|
||||
convex: "https://convex.dev",
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
**Logo configuration:**
|
||||
|
||||
- `logo`: Homepage logo path (set to `null` to hide). Uses `public/images/logo.svg` by default.
|
||||
- `innerPageLogo`: Logo shown on blog page, posts, and static pages. Desktop: top left. Mobile: top right. Set `enabled: false` to hide on inner pages while keeping homepage logo.
|
||||
|
||||
**Navigation structure:**
|
||||
|
||||
Navigation combines three sources sorted by `order`:
|
||||
|
||||
1. Blog link (if `blogPage.enabled` and `blogPage.showInNav` are true)
|
||||
2. Hardcoded nav items (React routes from `hardcodedNavItems`)
|
||||
3. Markdown pages (from `content/pages/` with `showInNav: true`)
|
||||
|
||||
All items sort by `order` (lower first), then alphabetically by title.
|
||||
|
||||
### Featured items
|
||||
|
||||
Posts and pages appear in the featured section when marked with `featured: true` in frontmatter.
|
||||
|
||||
**Add to featured section:**
|
||||
|
||||
```yaml
|
||||
# In any post or page frontmatter
|
||||
featured: true
|
||||
featuredOrder: 1
|
||||
excerpt: "Short description for card view."
|
||||
image: "/images/thumbnail.png"
|
||||
```
|
||||
|
||||
Then run `npm run sync` or `npm run sync:all`. 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.
|
||||
|
||||
**Ordering:** Items with `featuredOrder` appear first (lower numbers first). Items without `featuredOrder` appear after, sorted by creation time.
|
||||
|
||||
**Display options (in siteConfig):**
|
||||
|
||||
```typescript
|
||||
// In src/pages/Home.tsx
|
||||
const siteConfig = {
|
||||
featuredViewMode: "list", // 'list' or 'cards'
|
||||
showViewToggle: true, // Let users switch views
|
||||
};
|
||||
```
|
||||
|
||||
### GitHub contributions graph
|
||||
|
||||
Display your GitHub contribution activity on the homepage. Configure in `siteConfig`:
|
||||
|
||||
```typescript
|
||||
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 navigation |
|
||||
| `linkToProfile` | Click graph to visit GitHub profile |
|
||||
| `title` | Text above graph (`undefined` to hide) |
|
||||
|
||||
Theme-aware colors match each site theme. Uses public API (no GitHub token required).
|
||||
|
||||
### Visitor map
|
||||
|
||||
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.
|
||||
|
||||
```typescript
|
||||
visitorMap: {
|
||||
enabled: true, // Set to false to hide
|
||||
title: "Live Visitors", // Optional title above the map
|
||||
},
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| --------- | ------------------------------------ |
|
||||
| `enabled` | `true` to show, `false` to hide |
|
||||
| `title` | Text above map (`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.
|
||||
|
||||
### Logo gallery
|
||||
|
||||
The homepage includes a logo gallery that can scroll infinitely or display as a static grid. Each logo can link to a URL.
|
||||
|
||||
```typescript
|
||||
// In src/config/siteConfig.ts
|
||||
logoGallery: {
|
||||
enabled: true, // 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", // undefined to hide
|
||||
scrolling: false, // false = static grid, true = scrolling marquee
|
||||
maxItems: 4, // Number of logos when scrolling is false
|
||||
},
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ----------- | ---------------------------------------------------------- |
|
||||
| `enabled` | `true` to show, `false` to hide |
|
||||
| `images` | Array of `{ src, href }` objects |
|
||||
| `position` | `'above-footer'` or `'below-featured'` |
|
||||
| `speed` | Seconds for one scroll cycle (lower = faster) |
|
||||
| `title` | Text above gallery (`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: true`: Infinite horizontal scroll with all logos
|
||||
- `scrolling: false`: Static centered grid showing first `maxItems` logos
|
||||
|
||||
**To add logos:**
|
||||
|
||||
1. Add SVG/PNG files to `public/images/logos/`
|
||||
2. Update the `images` array with `src` paths and `href` URLs
|
||||
3. Push to GitHub (requires rebuild)
|
||||
|
||||
**To disable:** Set `enabled: false`
|
||||
|
||||
**To remove samples:** Delete files from `public/images/logos/` or clear the images array.
|
||||
|
||||
### Blog page
|
||||
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
blogPage: {
|
||||
enabled: true, // Enable /blog route
|
||||
showInNav: true, // Show in navigation
|
||||
title: "Blog", // Nav link and page title
|
||||
order: 0, // Nav order (lower = first)
|
||||
viewMode: "list", // Default view: "list" or "cards"
|
||||
showViewToggle: true, // Show toggle button to switch views
|
||||
},
|
||||
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) |
|
||||
| `viewMode` | Default view: `"list"` or `"cards"` |
|
||||
| `showViewToggle` | Show toggle button to switch views |
|
||||
| `displayOnHomepage` | Show post list on homepage |
|
||||
|
||||
**View modes:**
|
||||
|
||||
- **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.
|
||||
|
||||
**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.
|
||||
|
||||
**View preference:** User's view mode choice is saved to localStorage and persists across page visits.
|
||||
|
||||
### Scroll-to-top button
|
||||
|
||||
A scroll-to-top button appears after scrolling down. Configure in `src/components/Layout.tsx`:
|
||||
|
||||
```typescript
|
||||
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 |
|
||||
|
||||
Uses Phosphor ArrowUp icon and works with all themes.
|
||||
|
||||
### Theme
|
||||
|
||||
Default: `tan`. Options: `dark`, `light`, `tan`, `cloud`.
|
||||
|
||||
Configure in `src/config/siteConfig.ts`:
|
||||
|
||||
```typescript
|
||||
export const siteConfig: SiteConfig = {
|
||||
// ... other config
|
||||
defaultTheme: "tan",
|
||||
};
|
||||
```
|
||||
|
||||
### Font
|
||||
|
||||
Configure the font in `src/config/siteConfig.ts`:
|
||||
|
||||
```typescript
|
||||
export const siteConfig: SiteConfig = {
|
||||
// ... other config
|
||||
fontFamily: "serif", // Options: "serif", "sans", or "monospace"
|
||||
};
|
||||
```
|
||||
|
||||
Or edit `src/styles/global.css` directly:
|
||||
|
||||
```css
|
||||
body {
|
||||
/* Sans-serif */
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
/* Serif (default) */
|
||||
font-family: "New York", ui-serif, Georgia, serif;
|
||||
|
||||
/* Monospace */
|
||||
font-family: "IBM Plex Mono", "Liberation Mono", ui-monospace, monospace;
|
||||
}
|
||||
```
|
||||
|
||||
Available options: `serif` (default), `sans`, or `monospace`.
|
||||
|
||||
### Font sizes
|
||||
|
||||
All font sizes use CSS variables in `:root`. Customize by editing:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-size-base: 16px;
|
||||
--font-size-sm: 13px;
|
||||
--font-size-lg: 17px;
|
||||
--font-size-blog-content: 17px;
|
||||
--font-size-post-title: 32px;
|
||||
}
|
||||
```
|
||||
|
||||
Mobile sizes defined in `@media (max-width: 768px)` block.
|
||||
|
||||
### Images
|
||||
|
||||
| Image | Location | Size |
|
||||
| ---------------- | ------------------------------ | -------- |
|
||||
| Favicon | `public/favicon.svg` | 512x512 |
|
||||
| Site logo | `public/images/logo.svg` | 512x512 |
|
||||
| Default OG image | `public/images/og-default.svg` | 1200x630 |
|
||||
| Post images | `public/images/` | Any |
|
||||
|
||||
**Images require git deploy.** Images are served as static files from your repository, not synced to Convex. After adding images to `public/images/`:
|
||||
|
||||
1. Commit the image files to git
|
||||
2. Push to GitHub
|
||||
3. Wait for Netlify to rebuild
|
||||
|
||||
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.
|
||||
|
||||
**Adding images to posts:** You can add images using markdown syntax `` or HTML `<img>` tags. The site uses `rehypeRaw` and `rehypeSanitize` to safely render HTML in markdown content. See [Using Images in Blog Posts](/using-images-in-posts) for complete examples and best practices.
|
||||
|
||||
**Logo options:**
|
||||
|
||||
- **Homepage logo:** Configured via `logo` in `siteConfig.ts`. Set to `null` to hide.
|
||||
- **Inner page logo:** Configured via `innerPageLogo` in `siteConfig.ts`. Shows on blog page, posts, and static pages. Desktop: top left corner. Mobile: top right corner (smaller). Set `enabled: false` to hide on inner pages while keeping homepage logo.
|
||||
307
content/pages/docs-content.md
Normal file
307
content/pages/docs-content.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
title: "Content"
|
||||
slug: "docs-content"
|
||||
published: true
|
||||
order: 2
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 2
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Content
|
||||
|
||||
**Markdown examples:** For complete markdown syntax examples including code blocks, tables, lists, links, images, collapsible sections, and all formatting options, see [Writing Markdown with Code Examples](/markdown-with-code-examples). That post includes copy-paste examples for every markdown feature.
|
||||
|
||||
### Blog posts
|
||||
|
||||
Create files in `content/blog/` with frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Post Title"
|
||||
description: "SEO description"
|
||||
date: "2025-01-15"
|
||||
slug: "url-path"
|
||||
published: true
|
||||
tags: ["tag1", "tag2"]
|
||||
readTime: "5 min read"
|
||||
image: "/images/og-image.png"
|
||||
---
|
||||
|
||||
Content here...
|
||||
```
|
||||
|
||||
See the [Frontmatter](/docs-frontmatter) page for all available fields.
|
||||
|
||||
### Static pages
|
||||
|
||||
Create files in `content/pages/` with frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Page Title"
|
||||
slug: "url-path"
|
||||
published: true
|
||||
order: 1
|
||||
---
|
||||
|
||||
Content here...
|
||||
```
|
||||
|
||||
See the [Frontmatter](/docs-frontmatter) page for all available fields.
|
||||
|
||||
### Home intro content
|
||||
|
||||
The homepage intro text can be synced from markdown via `content/pages/home.md` (slug: `home-intro`). This allows you to update homepage text without redeploying.
|
||||
|
||||
**Create home intro:**
|
||||
|
||||
1. Create `content/pages/home.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Home Intro"
|
||||
slug: "home-intro"
|
||||
published: true
|
||||
showInNav: false
|
||||
order: -1
|
||||
textAlign: "left"
|
||||
---
|
||||
|
||||
Your homepage intro text here.
|
||||
|
||||
## Features
|
||||
|
||||
**Feature one** - Description here.
|
||||
|
||||
**Feature two** - Description here.
|
||||
```
|
||||
|
||||
2. Run `npm run sync` to sync to Convex
|
||||
|
||||
3. Content appears on homepage instantly (no rebuild needed)
|
||||
|
||||
**Blog heading styles:** Headings (h1-h6) in home intro content use the same styling as blog posts (`blog-h1` through `blog-h6` classes). Each heading gets an automatic ID and a clickable anchor link (#) that appears on hover. Lists, blockquotes, horizontal rules, and links also use blog styling classes for consistent typography.
|
||||
|
||||
**Fallback:** If `home-intro` page is not found, the homepage falls back to `siteConfig.bio` text.
|
||||
|
||||
### Footer content
|
||||
|
||||
The footer content can be synced from markdown via `content/pages/footer.md` (slug: `footer`). This allows you to update footer text without touching code.
|
||||
|
||||
**Create footer content:**
|
||||
|
||||
1. Create `content/pages/footer.md`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Footer"
|
||||
slug: "footer"
|
||||
published: true
|
||||
showInNav: false
|
||||
order: -1
|
||||
---
|
||||
|
||||
Built with [Convex](https://convex.dev) for real-time sync and deployed on [Netlify](https://netlify.com).
|
||||
|
||||
Created by [Your Name](https://x.com/yourhandle). Follow on [Twitter/X](https://x.com/yourhandle) and [GitHub](https://github.com/yourusername).
|
||||
```
|
||||
|
||||
2. Run `npm run sync` to sync to Convex
|
||||
|
||||
3. Footer content appears on homepage, blog page, and all posts/pages instantly (no rebuild needed)
|
||||
|
||||
**Markdown support:** Footer content supports full markdown including links, paragraphs, line breaks, and images. External links automatically open in new tabs.
|
||||
|
||||
**Fallback:** If `footer` page is not found, the footer falls back to `siteConfig.footer.defaultContent`.
|
||||
|
||||
**Priority order:** Per-post/page frontmatter `footer:` field (custom override) > synced footer.md content > siteConfig.footer.defaultContent.
|
||||
|
||||
**Relationship with siteConfig:** The `content/pages/footer.md` page takes priority over `siteConfig.footer.defaultContent` when present. Use the markdown page for dynamic content that changes frequently, or keep using siteConfig for static footer content.
|
||||
|
||||
### Sidebar layout
|
||||
|
||||
Posts and pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Documentation"
|
||||
slug: "docs"
|
||||
published: true
|
||||
layout: "sidebar"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
## Section One
|
||||
|
||||
### Subsection
|
||||
|
||||
## Section Two
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Left sidebar displays table of contents extracted from H1, H2, H3 headings
|
||||
- Two-column layout: 220px sidebar + flexible content area
|
||||
- Sidebar only appears if headings exist in the content
|
||||
- Active heading highlighting as you scroll
|
||||
- Smooth scroll navigation when clicking TOC links
|
||||
- Mobile responsive: stacks to single column below 1024px
|
||||
- Works for both blog posts and static pages
|
||||
|
||||
The sidebar extracts headings automatically from your markdown content. No manual TOC needed.
|
||||
|
||||
### Right sidebar
|
||||
|
||||
When enabled in `siteConfig.rightSidebar.enabled`, posts and pages can display a right sidebar containing the CopyPageDropdown at 1135px+ viewport width.
|
||||
|
||||
**Configuration:**
|
||||
|
||||
Enable globally in `src/config/siteConfig.ts`:
|
||||
|
||||
```typescript
|
||||
rightSidebar: {
|
||||
enabled: true, // Set to false to disable right sidebar globally
|
||||
minWidth: 1135, // Minimum viewport width to show sidebar
|
||||
},
|
||||
```
|
||||
|
||||
Control per post/page with frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "My Post"
|
||||
rightSidebar: true # Enable right sidebar for this post
|
||||
---
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Right sidebar appears at 1135px+ viewport width
|
||||
- Contains CopyPageDropdown with all sharing options
|
||||
- Three-column layout: left sidebar (TOC), main content, right sidebar
|
||||
- CopyPageDropdown automatically moves from nav to right sidebar when enabled
|
||||
- Hidden below 1135px breakpoint, CopyPageDropdown returns to nav
|
||||
- Per-post/page control via `rightSidebar: true` frontmatter field
|
||||
- Opt-in only: right sidebar only appears when explicitly enabled in frontmatter
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Keep CopyPageDropdown accessible on wide screens without cluttering the nav
|
||||
- Provide quick access to sharing options while reading long content
|
||||
- Works alongside left sidebar TOC for comprehensive navigation
|
||||
|
||||
**Example for blog post:**
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "My Tutorial"
|
||||
description: "A detailed guide"
|
||||
date: "2025-01-20"
|
||||
slug: "my-tutorial"
|
||||
published: true
|
||||
tags: ["tutorial"]
|
||||
layout: "sidebar"
|
||||
---
|
||||
|
||||
# Introduction
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
## Advanced Topics
|
||||
```
|
||||
|
||||
### How frontmatter works
|
||||
|
||||
Frontmatter is the YAML metadata at the top of each markdown file between `---` markers. Here is how it flows through the system:
|
||||
|
||||
**Content directories:**
|
||||
|
||||
- `content/blog/*.md` contains blog posts with frontmatter
|
||||
- `content/pages/*.md` contains static pages with frontmatter
|
||||
|
||||
**Processing flow:**
|
||||
|
||||
1. Markdown files in `content/blog/` and `content/pages/` contain YAML frontmatter
|
||||
2. `scripts/sync-posts.ts` uses `gray-matter` to parse frontmatter and validate required fields
|
||||
3. Parsed data is sent to Convex mutations (`api.posts.syncPostsPublic`, `api.pages.syncPagesPublic`)
|
||||
4. `convex/schema.ts` defines the database structure for storing the data
|
||||
|
||||
**Adding a new frontmatter field:**
|
||||
|
||||
To add a custom frontmatter field, update these files:
|
||||
|
||||
1. The interface in `scripts/sync-posts.ts` (`PostFrontmatter` or `PageFrontmatter`)
|
||||
2. The parsing logic in `parseMarkdownFile()` or `parsePageFile()` functions
|
||||
3. The schema in `convex/schema.ts`
|
||||
4. The sync mutation in `convex/posts.ts` or `convex/pages.ts`
|
||||
|
||||
### Syncing content
|
||||
|
||||
**Development:**
|
||||
|
||||
```bash
|
||||
npm run sync # Sync markdown content
|
||||
npm run sync:discovery # Update discovery files (AGENTS.md, llms.txt)
|
||||
npm run sync:all # Sync content + discovery files together
|
||||
```
|
||||
|
||||
**Production:**
|
||||
|
||||
```bash
|
||||
npm run sync:prod # Sync markdown content
|
||||
npm run sync:discovery:prod # Update discovery files
|
||||
npm run sync:all:prod # Sync content + discovery files together
|
||||
```
|
||||
|
||||
**Sync everything together:**
|
||||
|
||||
```bash
|
||||
npm run sync:all # Development: content + discovery
|
||||
npm run sync:all:prod # Production: content + discovery
|
||||
```
|
||||
|
||||
### 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) |
|
||||
| Site config changes | `npm run sync:discovery` | Updates discovery files |
|
||||
| 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 to Convex. **Images and source code** require pushing to GitHub for Netlify to rebuild.
|
||||
|
||||
## Tag pages and related posts
|
||||
|
||||
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 with localStorage persistence for view mode preference.
|
||||
|
||||
**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.
|
||||
|
||||
## Blog page featured layout
|
||||
|
||||
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.
|
||||
400
content/pages/docs-dashboard.md
Normal file
400
content/pages/docs-dashboard.md
Normal file
@@ -0,0 +1,400 @@
|
||||
---
|
||||
title: "Dashboard"
|
||||
slug: "docs-dashboard"
|
||||
published: true
|
||||
order: 5
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 5
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
The Dashboard at `/dashboard` provides a centralized UI for managing content, configuring the site, and performing sync operations. It's designed for developers who fork the repository to set up and manage their markdown blog.
|
||||
|
||||
**Access:** Navigate to `/dashboard` in your browser. The dashboard is not linked in the navigation by default (similar to Newsletter Admin pattern).
|
||||
|
||||
**Authentication:** WorkOS authentication is optional. Configure it in `siteConfig.ts`:
|
||||
|
||||
```typescript
|
||||
dashboard: {
|
||||
enabled: true,
|
||||
requireAuth: false, // Set to true to require WorkOS authentication
|
||||
},
|
||||
```
|
||||
|
||||
When `requireAuth` is `false`, the dashboard is open access. When `requireAuth` is `true` and WorkOS is configured, users must log in to access the dashboard. See [How to setup WorkOS](https://www.markdown.fast/how-to-setup-workos) for authentication setup.
|
||||
|
||||
### Content management
|
||||
|
||||
**Posts and Pages List Views:**
|
||||
|
||||
- View all posts and pages (published and unpublished)
|
||||
- Filter by status: All, Published, Drafts
|
||||
- Search by title or content
|
||||
- Pagination with "First" and "Next" buttons
|
||||
- Items per page selector (15, 25, 50, 100) - default: 15
|
||||
- Edit, view, and publish/unpublish options
|
||||
- WordPress-style UI with date, edit, view, and publish controls
|
||||
- **Source Badge:** Shows "Dashboard" or "Synced" to indicate content origin
|
||||
- **Delete Button:** Delete dashboard-created posts/pages directly (synced content protected)
|
||||
|
||||
**Post and Page Editor:**
|
||||
|
||||
- Markdown editor with live preview
|
||||
- Frontmatter sidebar on the right with all available fields
|
||||
- Draggable/resizable frontmatter sidebar (200px-600px width)
|
||||
- Independent scrolling for frontmatter sidebar
|
||||
- Preview mode shows content as it appears on the live site
|
||||
- Download markdown button to generate `.md` files
|
||||
- Copy markdown to clipboard
|
||||
- All frontmatter fields editable in sidebar
|
||||
- Preview uses ReactMarkdown with proper styling
|
||||
- **Save to Database:** Green "Save" button saves changes directly to the database
|
||||
|
||||
**Write Post and Write Page:**
|
||||
|
||||
- Full-screen writing interface
|
||||
- **Three Editor Modes:**
|
||||
- **Markdown:** Raw markdown editing (default)
|
||||
- **Rich Text:** WYSIWYG Quill editor with toolbar
|
||||
- **Preview:** Rendered markdown preview
|
||||
- Word/line/character counts
|
||||
- Frontmatter reference panel
|
||||
- Download markdown button for new content
|
||||
- **Save to DB:** Save directly to database without file sync
|
||||
- Content persists in localStorage
|
||||
- Separate storage for post and page content
|
||||
|
||||
### Cloud CMS features
|
||||
|
||||
The dashboard functions as a cloud-based CMS similar to WordPress, allowing you to create and edit content directly in the database without requiring the markdown file sync workflow.
|
||||
|
||||
**Dual Source Architecture:**
|
||||
|
||||
- **Dashboard Content:** Posts/pages created via "Save to DB" are marked with `source: "dashboard"`
|
||||
- **Synced Content:** Posts/pages from markdown files are marked with `source: "sync"`
|
||||
- Both coexist independently in the database
|
||||
- Sync operations only affect synced content (dashboard content is protected)
|
||||
|
||||
**Direct Database Operations:**
|
||||
|
||||
- Create new posts/pages directly in the database
|
||||
- Edit any post/page and save changes immediately
|
||||
- Delete dashboard-created content
|
||||
- Changes appear instantly (no sync required)
|
||||
|
||||
**Export to Markdown:**
|
||||
|
||||
- Any post/page can be exported as a `.md` file
|
||||
- Includes all frontmatter fields
|
||||
- Use for backup or converting to file-based workflow
|
||||
|
||||
**Bulk Export Script:**
|
||||
|
||||
```bash
|
||||
npm run export:db # Export dashboard posts to content/blog/
|
||||
npm run export:db:prod # Export from production database
|
||||
```
|
||||
|
||||
Exports all dashboard-created posts and pages to markdown files in the content folders.
|
||||
|
||||
### Rich Text Editor
|
||||
|
||||
The Write Post and Write Page sections include a Quill-based rich text editor with three editing modes.
|
||||
|
||||
**Editing Modes:**
|
||||
|
||||
- **Markdown:** Raw markdown text editing (default mode)
|
||||
- **Rich Text:** WYSIWYG editor with formatting toolbar
|
||||
- **Preview:** Rendered preview of the content
|
||||
|
||||
**Rich Text Toolbar:**
|
||||
|
||||
- Headers (H1, H2, H3)
|
||||
- Bold, italic, strikethrough
|
||||
- Blockquote, code block
|
||||
- Ordered and bullet lists
|
||||
- Links
|
||||
- Clear formatting
|
||||
|
||||
**Mode Switching:**
|
||||
|
||||
- Content automatically converts between HTML and Markdown when switching modes
|
||||
- Frontmatter is preserved when editing in Rich Text mode
|
||||
- Preview mode shows how content will appear on the live site
|
||||
|
||||
**Theme Integration:**
|
||||
|
||||
- Editor styling matches the current theme (dark, light, tan, cloud)
|
||||
- Toolbar uses CSS variables for consistent appearance
|
||||
|
||||
### AI Agent
|
||||
|
||||
The Dashboard includes a dedicated AI Agent section with tab-based UI for Chat and Image Generation.
|
||||
|
||||
**Chat Tab:**
|
||||
|
||||
- Multi-model selector: Claude Sonnet 4, GPT-4o, Gemini 2.0 Flash
|
||||
- Per-session chat history stored in Convex
|
||||
- Markdown rendering for AI responses
|
||||
- Copy functionality for AI responses
|
||||
- Lazy API key validation (errors only shown when user tries to use a specific model)
|
||||
|
||||
**Image Tab:**
|
||||
|
||||
- AI image generation with two models:
|
||||
- Nano Banana (gemini-2.0-flash-exp-image-generation) - Experimental model
|
||||
- Nano Banana Pro (imagen-3.0-generate-002) - Production model
|
||||
- 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
|
||||
|
||||
**Environment Variables (Convex):**
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------- | -------------------------------------------------- |
|
||||
| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 |
|
||||
| `OPENAI_API_KEY` | Required for GPT-4o |
|
||||
| `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation |
|
||||
|
||||
**Note:** Only configure the API keys for models you want to use. If a key is not set, users see a helpful setup message when they try to use that model.
|
||||
|
||||
### Newsletter management
|
||||
|
||||
All Newsletter Admin features integrated into the Dashboard:
|
||||
|
||||
- **Subscribers:** View, search, filter, and delete subscribers
|
||||
- **Send Newsletter:** Select a blog post to send as newsletter
|
||||
- **Write Email:** Compose custom emails with markdown support
|
||||
- **Recent Sends:** View last 10 newsletter sends (posts and custom emails)
|
||||
- **Email Stats:** Dashboard with total emails, newsletters sent, active subscribers, retention rate
|
||||
|
||||
All newsletter sections are full-width in the dashboard content area.
|
||||
|
||||
### Content import
|
||||
|
||||
**Direct Database Import:**
|
||||
|
||||
The Import URL section uses server-side Firecrawl to import articles directly to the database.
|
||||
|
||||
- Enter any article URL to import
|
||||
- Firecrawl scrapes and converts content to markdown
|
||||
- Post is saved directly to the database (no file sync needed)
|
||||
- Optional "Publish immediately" checkbox
|
||||
- Imported posts tagged with `imported` by default
|
||||
- Source attribution added automatically
|
||||
- Success message with link to view the imported post
|
||||
|
||||
**Setup:**
|
||||
|
||||
Add `FIRECRAWL_API_KEY` to your Convex environment variables:
|
||||
|
||||
```bash
|
||||
npx convex env set FIRECRAWL_API_KEY your-api-key-here
|
||||
```
|
||||
|
||||
Get your API key from [firecrawl.dev](https://firecrawl.dev).
|
||||
|
||||
**CLI Import (Alternative):**
|
||||
|
||||
You can also import via command line:
|
||||
|
||||
```bash
|
||||
npm run import <url> # Import URL as local markdown file
|
||||
```
|
||||
|
||||
This creates a file in `content/blog/` that requires syncing.
|
||||
|
||||
### Site configuration
|
||||
|
||||
**Config Generator:**
|
||||
|
||||
- UI to configure all settings in `src/config/siteConfig.ts`
|
||||
- Generates downloadable `siteConfig.ts` file
|
||||
- Hybrid approach: dashboard generates config, file-based config continues to work
|
||||
- Includes all site configuration options:
|
||||
- Site name, title, logo, bio, intro
|
||||
- Blog page settings
|
||||
- Featured section configuration
|
||||
- Logo gallery settings
|
||||
- GitHub contributions
|
||||
- Footer and social footer
|
||||
- Newsletter settings
|
||||
- Contact form settings
|
||||
- Stats page settings
|
||||
- And more
|
||||
|
||||
**Index HTML Editor:**
|
||||
|
||||
- View and edit `index.html` content
|
||||
- Meta tags, Open Graph, Twitter Cards, JSON-LD
|
||||
- Download updated HTML file
|
||||
|
||||
### Analytics
|
||||
|
||||
- Real-time stats dashboard (clone of `/stats` page)
|
||||
- Active visitors with per-page breakdown
|
||||
- Total page views and unique visitors
|
||||
- Views by page sorted by popularity
|
||||
- Does not follow `siteConfig.statsPage` settings (always accessible in dashboard)
|
||||
|
||||
### Sync commands
|
||||
|
||||
**Sync Content Section:**
|
||||
|
||||
- UI with buttons for all sync operations
|
||||
- Development sync commands:
|
||||
- `npm run sync` - Sync markdown content
|
||||
- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt)
|
||||
- `npm run sync:all` - Sync content + discovery files together
|
||||
- Production sync commands:
|
||||
- `npm run sync:prod` - Sync markdown content
|
||||
- `npm run sync:discovery:prod` - Update discovery files
|
||||
- `npm run sync:all:prod` - Sync content + discovery files together
|
||||
- Server status indicator shows if sync server is online
|
||||
- Copy and Execute buttons for each command
|
||||
- Real-time terminal output when sync server is running
|
||||
- Command modal shows full command output when sync server is offline
|
||||
- Toast notifications for success/error feedback
|
||||
|
||||
**Sync Server:**
|
||||
|
||||
- Local HTTP server for executing commands from dashboard
|
||||
- Start with `npm run sync-server` (runs on localhost:3001)
|
||||
- Execute commands directly from dashboard with real-time output streaming
|
||||
- Optional token authentication via `SYNC_TOKEN` environment variable
|
||||
- Whitelisted commands only for security
|
||||
- Health check endpoint for server availability detection
|
||||
- Copy icons for `npm run sync-server` command in dashboard
|
||||
|
||||
**Header Sync Buttons:**
|
||||
|
||||
- Quick sync buttons in dashboard header (right side)
|
||||
- `npm run sync:all` (dev) button
|
||||
- `npm run sync:all:prod` (prod) button
|
||||
- One-click sync for all content and discovery files
|
||||
- Automatically use sync server when available, fallback to command modal
|
||||
|
||||
### Dashboard features
|
||||
|
||||
**Search:**
|
||||
|
||||
- Search bar in header
|
||||
- Search dashboard features, page titles, and post content
|
||||
- Real-time results as you type
|
||||
|
||||
**Theme and Font:**
|
||||
|
||||
- Theme toggle (dark, light, tan, cloud)
|
||||
- Font switcher (serif, sans, monospace)
|
||||
- Preferences persist across sessions
|
||||
|
||||
**Mobile Responsive:**
|
||||
|
||||
- Fully responsive design
|
||||
- Mobile-optimized layout
|
||||
- Touch-friendly controls
|
||||
- Collapsible sidebar on mobile
|
||||
|
||||
**Toast Notifications:**
|
||||
|
||||
- Success, error, info, and warning notifications
|
||||
- Auto-dismiss after 4 seconds
|
||||
- Theme-aware styling
|
||||
- No browser default alerts
|
||||
|
||||
**Command Modal:**
|
||||
|
||||
- Shows sync command output
|
||||
- Copy command to clipboard
|
||||
- Close button to dismiss
|
||||
- Theme-aware styling
|
||||
|
||||
### Technical details
|
||||
|
||||
**Database Architecture:**
|
||||
|
||||
- Uses Convex queries for real-time data
|
||||
- All mutations follow Convex best practices (idempotent, indexed queries)
|
||||
- `source` field tracks content origin ("dashboard" or "sync")
|
||||
- `by_source` index for efficient filtering by source
|
||||
|
||||
**CMS Mutations (convex/cms.ts):**
|
||||
|
||||
- `createPost` / `createPage` - Create with `source: "dashboard"`
|
||||
- `updatePost` / `updatePage` - Update any post/page
|
||||
- `deletePost` / `deletePage` - Delete any post/page
|
||||
- `exportPostAsMarkdown` / `exportPageAsMarkdown` - Generate markdown with frontmatter
|
||||
|
||||
**Import Action (convex/importAction.ts):**
|
||||
|
||||
- Server-side Convex action using Firecrawl
|
||||
- Scrapes URL, converts to markdown, saves to database
|
||||
- Handles slug conflicts by appending timestamp
|
||||
|
||||
**UI State:**
|
||||
|
||||
- Frontmatter sidebar width persisted in localStorage
|
||||
- Editor content persisted in localStorage
|
||||
- Independent scrolling for editor and sidebar sections
|
||||
- Preview uses ReactMarkdown with remark-gfm, remark-breaks, rehype-raw, rehype-sanitize
|
||||
- Rich text editor uses Quill with Turndown/Showdown for conversion
|
||||
|
||||
### Sync commands reference
|
||||
|
||||
**Development:**
|
||||
|
||||
- `npm run sync` - Sync markdown content to development Convex
|
||||
- `npm run sync:discovery` - Update discovery files (AGENTS.md, llms.txt) with development data
|
||||
- `npm run sync:all` - Run both content sync and discovery sync (development)
|
||||
|
||||
**Production:**
|
||||
|
||||
- `npm run sync:prod` - Sync markdown content to production Convex
|
||||
- `npm run sync:discovery:prod` - Update discovery files with production data
|
||||
- `npm run sync:all:prod` - Run both content sync and discovery sync (production)
|
||||
|
||||
**Sync Server:**
|
||||
|
||||
- `npm run sync-server` - Start local HTTP server for executing sync commands from dashboard UI
|
||||
|
||||
**Content Import:**
|
||||
|
||||
- `npm run import <url>` - Import external URL as markdown post (requires FIRECRAWL_API_KEY in .env.local)
|
||||
|
||||
**Database Export:**
|
||||
|
||||
- `npm run export:db` - Export dashboard posts/pages to content folders (development)
|
||||
- `npm run export:db:prod` - Export dashboard posts/pages (production)
|
||||
|
||||
**Note:** The dashboard provides a UI for these commands. When the sync server is running (`npm run sync-server`), you can execute commands directly from the dashboard with real-time output. Otherwise, the dashboard shows commands in a modal for copying to your terminal.
|
||||
|
||||
### Environment variables
|
||||
|
||||
**Convex Environment Variables:**
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------- | -------------------------------------------------- |
|
||||
| `ANTHROPIC_API_KEY` | Required for Claude Sonnet 4 (AI Agent) |
|
||||
| `OPENAI_API_KEY` | Required for GPT-4o (AI Agent) |
|
||||
| `GOOGLE_AI_API_KEY` | Required for Gemini 2.0 Flash and image generation |
|
||||
| `FIRECRAWL_API_KEY` | Required for direct URL import |
|
||||
|
||||
Set Convex environment variables with:
|
||||
|
||||
```bash
|
||||
npx convex env set VARIABLE_NAME value
|
||||
```
|
||||
|
||||
**Local Environment Variables (.env.local):**
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `VITE_CONVEX_URL` | Your Convex deployment URL (auto-created) |
|
||||
| `FIRECRAWL_API_KEY` | For CLI import command only |
|
||||
106
content/pages/docs-deployment.md
Normal file
106
content/pages/docs-deployment.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
title: "Deployment"
|
||||
slug: "docs-deployment"
|
||||
published: true
|
||||
order: 6
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 6
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
### Netlify setup
|
||||
|
||||
1. Connect GitHub repo to Netlify
|
||||
2. Build command: `npm ci --include=dev && npx convex deploy --cmd 'npm run build'`
|
||||
3. Publish directory: `dist`
|
||||
4. Add env variables:
|
||||
- `CONVEX_DEPLOY_KEY` (from Convex Dashboard > Project Settings > Deploy Key)
|
||||
- `VITE_CONVEX_URL` (your production Convex URL, e.g., `https://your-deployment.convex.cloud`)
|
||||
|
||||
Both are required: deploy key for builds, URL for edge function runtime.
|
||||
|
||||
### Convex production
|
||||
|
||||
```bash
|
||||
npx convex deploy
|
||||
```
|
||||
|
||||
### Edge functions
|
||||
|
||||
RSS, sitemap, and API routes are handled by Netlify Edge Functions in `netlify/edge-functions/`. They dynamically read `VITE_CONVEX_URL` from the environment. No manual URL configuration needed.
|
||||
|
||||
## Convex schema
|
||||
|
||||
```typescript
|
||||
// convex/schema.ts
|
||||
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()), // For card view
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
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()), // For card view
|
||||
image: v.optional(v.string()), // Thumbnail for featured cards
|
||||
featured: v.optional(v.boolean()), // Show in featured section
|
||||
featuredOrder: v.optional(v.number()), // Order in featured (lower = first)
|
||||
authorName: v.optional(v.string()), // Author display name
|
||||
authorImage: v.optional(v.string()), // Author avatar image URL
|
||||
lastSyncedAt: v.number(),
|
||||
})
|
||||
.index("by_slug", ["slug"])
|
||||
.index("by_published", ["published"])
|
||||
.index("by_featured", ["featured"]),
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Posts not appearing**
|
||||
|
||||
- Check `published: true` in frontmatter
|
||||
- Run `npm run sync` for development
|
||||
- Run `npm run sync:prod` for production
|
||||
- Use `npm run sync:all` or `npm run sync:all:prod` to sync content and update discovery files together
|
||||
- Verify in Convex dashboard
|
||||
|
||||
**RSS/Sitemap errors**
|
||||
|
||||
- Verify `VITE_CONVEX_URL` is set in Netlify
|
||||
- Test Convex HTTP URL: `https://your-deployment.convex.site/rss.xml`
|
||||
- Check edge functions in `netlify/edge-functions/`
|
||||
|
||||
**Build failures**
|
||||
|
||||
- Verify `CONVEX_DEPLOY_KEY` is set in Netlify
|
||||
- Ensure `@types/node` is in devDependencies
|
||||
- Build command must include `--include=dev`
|
||||
- Check Node.js version (18+)
|
||||
121
content/pages/docs-frontmatter.md
Normal file
121
content/pages/docs-frontmatter.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
title: "Frontmatter"
|
||||
slug: "docs-frontmatter"
|
||||
published: true
|
||||
order: 3
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 3
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Frontmatter is the YAML metadata at the top of each markdown file between `---` markers. It controls how content is displayed, organized, and discovered.
|
||||
|
||||
## Blog post fields
|
||||
|
||||
| Field | Required | Description |
|
||||
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `title` | Yes | Post title |
|
||||
| `description` | Yes | SEO description |
|
||||
| `date` | Yes | YYYY-MM-DD format |
|
||||
| `slug` | Yes | URL path (unique) |
|
||||
| `published` | Yes | `true` to show |
|
||||
| `tags` | Yes | Array of strings |
|
||||
| `readTime` | No | Display time estimate |
|
||||
| `image` | No | OG image and featured card thumbnail. See [Using Images in Blog Posts](/using-images-in-posts) for markdown and HTML syntax |
|
||||
| `showImageAtTop` | No | Set `true` to display the image at the top of the post above the header (default: `false`) |
|
||||
| `excerpt` | No | Short text for card view |
|
||||
| `featured` | No | `true` to show in featured section |
|
||||
| `featuredOrder` | No | Order in featured (lower = first) |
|
||||
| `authorName` | No | Author display name shown next to date |
|
||||
| `authorImage` | No | Round author avatar image URL |
|
||||
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
|
||||
| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) |
|
||||
| `showFooter` | No | Show footer on this post (overrides siteConfig default) |
|
||||
| `footer` | No | Per-post footer markdown (overrides `footer.md` and siteConfig.defaultContent) |
|
||||
| `showSocialFooter` | No | Show social footer on this post (overrides siteConfig default) |
|
||||
| `aiChat` | No | Enable AI chat in right sidebar. Set `true` to enable (requires `rightSidebar: true` and `siteConfig.aiChat.enabledOnContent: true`). Set `false` to explicitly hide even if global config is enabled. |
|
||||
| `blogFeatured` | No | Show as featured on blog page (first becomes hero, rest in 2-column row) |
|
||||
| `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) |
|
||||
| `contactForm` | No | Enable contact form on this post |
|
||||
| `unlisted` | No | Hide from listings but allow direct access via slug. Set `true` to hide from blog listings, featured sections, tag pages, search results, and related posts. Post remains accessible via direct link. |
|
||||
| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. |
|
||||
| `docsSectionGroup` | No | Group name for docs sidebar. Posts with the same group name appear together. |
|
||||
| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. |
|
||||
| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. |
|
||||
| `docsSectionGroupIcon` | No | Phosphor icon name for docs sidebar group (e.g., "Rocket", "Book", "PuzzlePiece"). Icon appears left of the group title. See [Phosphor Icons](https://phosphoricons.com) for available icons. |
|
||||
| `docsLanding` | No | Set `true` to use this post as the docs landing page (shown when navigating to `/docs`). |
|
||||
|
||||
## Page fields
|
||||
|
||||
| Field | Required | Description |
|
||||
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `title` | Yes | Nav link text |
|
||||
| `slug` | Yes | URL path |
|
||||
| `published` | Yes | `true` to show |
|
||||
| `order` | No | Nav order (lower = first) |
|
||||
| `showInNav` | No | Show in navigation menu (default: `true`) |
|
||||
| `excerpt` | No | Short text for card view |
|
||||
| `image` | No | Thumbnail for featured card view |
|
||||
| `showImageAtTop` | No | Set `true` to display the image at the top of the page above the header (default: `false`) |
|
||||
| `featured` | No | `true` to show in featured section |
|
||||
| `featuredOrder` | No | Order in featured (lower = first) |
|
||||
| `authorName` | No | Author display name shown next to date |
|
||||
| `authorImage` | No | Round author avatar image URL |
|
||||
| `layout` | No | Set to `"sidebar"` for docs-style layout with TOC |
|
||||
| `rightSidebar` | No | Enable right sidebar with CopyPageDropdown (opt-in, requires explicit `true`) |
|
||||
| `showFooter` | No | Show footer on this page (overrides siteConfig default) |
|
||||
| `footer` | No | Per-page footer markdown (overrides `footer.md` and siteConfig.defaultContent) |
|
||||
| `showSocialFooter` | No | Show social footer on this page (overrides siteConfig default) |
|
||||
| `aiChat` | No | Enable AI chat in right sidebar. Set `true` to enable (requires `rightSidebar: true` and `siteConfig.aiChat.enabledOnContent: true`). Set `false` to explicitly hide even if global config is enabled. |
|
||||
| `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) |
|
||||
| `contactForm` | No | Enable contact form on this page |
|
||||
| `textAlign` | No | Text alignment: "left" (default), "center", or "right". Used by `home.md` for home intro alignment |
|
||||
| `docsSection` | No | Include in docs sidebar. Set `true` to show in the docs section navigation. |
|
||||
| `docsSectionGroup` | No | Group name for docs sidebar. Pages with the same group name appear together. |
|
||||
| `docsSectionOrder` | No | Order within docs group. Lower numbers appear first within the group. |
|
||||
| `docsSectionGroupOrder` | No | Order of the group in docs sidebar. Lower numbers make the group appear first. Groups without this field sort alphabetically. |
|
||||
| `docsSectionGroupIcon` | No | Phosphor icon name for docs sidebar group (e.g., "Rocket", "Book", "PuzzlePiece"). Icon appears left of the group title. See [Phosphor Icons](https://phosphoricons.com) for available icons. |
|
||||
| `docsLanding` | No | Set `true` to use this page as the docs landing page (shown when navigating to `/docs`). |
|
||||
|
||||
## Common patterns
|
||||
|
||||
### Hide pages from navigation
|
||||
|
||||
Set `showInNav: false` to keep a page published and accessible via direct URL, but hidden from the navigation menu. Pages with `showInNav: false` remain searchable and available via API endpoints. Useful for pages you want to link directly but not show in the main nav.
|
||||
|
||||
### Unlisted posts
|
||||
|
||||
Set `unlisted: true` to hide a blog post from all listings while keeping it accessible via direct link. Unlisted posts are excluded from: blog listings (`/blog` page), featured sections (homepage), tag pages (`/tags/[tag]`), search results (Command+K), and related posts. The post remains accessible via direct URL (e.g., `/blog/post-slug`). Useful for draft posts, private content, or posts you want to share via direct link only. Note: `unlisted` only works for blog posts, not pages.
|
||||
|
||||
### 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.
|
||||
|
||||
### Image lightbox
|
||||
|
||||
Images in blog posts and pages automatically open in a full-screen lightbox when clicked (if enabled in `siteConfig.imageLightbox.enabled`). This allows readers to view images at full size. The lightbox can be closed by clicking outside the image, pressing Escape, or clicking the close button.
|
||||
|
||||
### Text alignment
|
||||
|
||||
Use `textAlign` field to control text alignment for page content. Options: `"left"` (default), `"center"`, or `"right"`. Used by `home.md` to control home intro alignment.
|
||||
|
||||
### Docs section
|
||||
|
||||
To add content to the docs sidebar:
|
||||
|
||||
1. Add `docsSection: true` to frontmatter
|
||||
2. Optionally set `docsSectionGroup` to group related content
|
||||
3. Use `docsSectionOrder` to control order within groups
|
||||
4. Use `docsSectionGroupOrder` to control group order
|
||||
5. Add `docsSectionGroupIcon` for visual icons (Phosphor icons)
|
||||
|
||||
### Docs landing page
|
||||
|
||||
Set `docsLanding: true` on one post or page to make it the docs landing page. This content displays when navigating to `/docs`.
|
||||
232
content/pages/docs-search.md
Normal file
232
content/pages/docs-search.md
Normal file
@@ -0,0 +1,232 @@
|
||||
---
|
||||
title: "Search"
|
||||
slug: "docs-search"
|
||||
published: true
|
||||
order: 2
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 3
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Keyword Search
|
||||
|
||||
Keyword search matches exact words using Convex full-text search. Results update instantly as you type.
|
||||
|
||||
For meaning-based search that finds conceptually similar content, see [Semantic Search](/docs-semantic-search).
|
||||
|
||||
---
|
||||
|
||||
### Keyboard shortcuts
|
||||
|
||||
Press `Cmd+K` (Mac) or `Ctrl+K` (Windows/Linux) to open search. Click the magnifying glass icon works too.
|
||||
|
||||
| Key | Action |
|
||||
| --- | ------ |
|
||||
| `Cmd+K` / `Ctrl+K` | Open/close search |
|
||||
| `Tab` | Switch between Keyword and Semantic modes |
|
||||
| `↑` `↓` | Navigate results |
|
||||
| `Enter` | Select result |
|
||||
| `Esc` | Close modal |
|
||||
|
||||
### How keyword search works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ KEYWORD SEARCH FLOW │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────┐ ┌─────────────┐ ┌──────────────────┐
|
||||
│ Cmd+K │───▶│ SearchModal │───▶│ Convex Query │
|
||||
└──────────┘ └─────────────┘ └────────┬─────────┘
|
||||
│
|
||||
┌──────────────────────┴──────────────────────┐
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ search_title │ │ search_content │
|
||||
│ (index) │ │ (index) │
|
||||
└───────┬────────┘ └───────┬────────┘
|
||||
│ │
|
||||
└──────────────────┬─────────────────────────┘
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Dedupe + Rank │
|
||||
│ (title matches first)│
|
||||
└──────────┬──────────┘
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Search Results │
|
||||
│ (max 15) │
|
||||
└──────────┬──────────┘
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Navigate + ?q=term │
|
||||
└──────────┬──────────┘
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Highlight matches │
|
||||
│ + scroll to first │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
1. User presses `Cmd+K` to open SearchModal
|
||||
2. SearchModal sends reactive query to Convex as user types
|
||||
3. Convex searches both title and content indexes in parallel
|
||||
4. Results are deduplicated (same post can match both indexes)
|
||||
5. Results ranked with title matches first, limited to 15
|
||||
6. User selects result, navigates with `?q=searchterm` param
|
||||
7. Destination page highlights all matches and scrolls to first
|
||||
|
||||
### Why keyword search
|
||||
|
||||
Keyword search is the default because it's:
|
||||
|
||||
- **Instant** - Results return in milliseconds, no API calls
|
||||
- **Free** - No external services, no per-query costs
|
||||
- **Reactive** - Updates in real-time as you type
|
||||
- **Highlightable** - Matches exact words, so results can be highlighted on the destination page
|
||||
|
||||
Use keyword search when you know the exact terms, code snippets, or commands you're looking for.
|
||||
|
||||
### How search indexes work
|
||||
|
||||
Search indexes are defined in `convex/schema.ts` on the posts and pages tables:
|
||||
|
||||
```typescript
|
||||
posts: defineTable({
|
||||
title: v.string(),
|
||||
content: v.string(),
|
||||
published: v.boolean(),
|
||||
// ... other fields
|
||||
})
|
||||
.searchIndex("search_content", {
|
||||
searchField: "content",
|
||||
filterFields: ["published"],
|
||||
})
|
||||
.searchIndex("search_title", {
|
||||
searchField: "title",
|
||||
filterFields: ["published"],
|
||||
}),
|
||||
```
|
||||
|
||||
Each table has two search indexes:
|
||||
|
||||
- `search_title` - Searches the title field
|
||||
- `search_content` - Searches the full markdown content
|
||||
|
||||
The `filterFields: ["published"]` allows filtering to only published content without a separate query.
|
||||
|
||||
### Search query implementation
|
||||
|
||||
The search query in `convex/search.ts` searches both titles and content, then deduplicates and ranks results:
|
||||
|
||||
```typescript
|
||||
// Search posts by title
|
||||
const postsByTitle = await ctx.db
|
||||
.query("posts")
|
||||
.withSearchIndex("search_title", (q) =>
|
||||
q.search("title", args.query).eq("published", true)
|
||||
)
|
||||
.take(10);
|
||||
|
||||
// Search posts by content
|
||||
const postsByContent = await ctx.db
|
||||
.query("posts")
|
||||
.withSearchIndex("search_content", (q) =>
|
||||
q.search("content", args.query).eq("published", true)
|
||||
)
|
||||
.take(10);
|
||||
```
|
||||
|
||||
Key features:
|
||||
|
||||
- **Dual search** - Searches both title and content indexes
|
||||
- **Filter by published** - Only returns published content
|
||||
- **Deduplication** - Removes duplicates when a post matches both title and content
|
||||
- **Ranking** - Title matches sort before content-only matches
|
||||
- **Snippets** - Generates context snippets around the search term
|
||||
- **Unlisted filtering** - Excludes posts with `unlisted: true`
|
||||
|
||||
### Frontend search modal
|
||||
|
||||
The `SearchModal` component (`src/components/SearchModal.tsx`) provides the UI:
|
||||
|
||||
```typescript
|
||||
// Reactive search query - updates as you type
|
||||
const results = useQuery(
|
||||
api.search.search,
|
||||
searchQuery.trim() ? { query: searchQuery } : "skip"
|
||||
);
|
||||
```
|
||||
|
||||
The `"skip"` parameter tells Convex to skip the query when the search field is empty, avoiding unnecessary database calls.
|
||||
|
||||
### Search result highlighting
|
||||
|
||||
When you click a search result, the app navigates to the page with a `?q=` parameter. The `useSearchHighlighting` hook (`src/hooks/useSearchHighlighting.ts`) then:
|
||||
|
||||
1. Waits for page content to load
|
||||
2. Finds all occurrences of the search term
|
||||
3. Wraps matches in `<mark>` tags with highlight styling
|
||||
4. Scrolls to the first match
|
||||
5. Highlights pulse, then fade after 4 seconds
|
||||
6. Press `Esc` to clear highlights manually
|
||||
|
||||
### Files involved
|
||||
|
||||
| File | Purpose |
|
||||
| ---- | ------- |
|
||||
| `convex/schema.ts` | Search index definitions |
|
||||
| `convex/search.ts` | Search query with deduplication and snippets |
|
||||
| `src/components/SearchModal.tsx` | Search UI with keyboard navigation |
|
||||
| `src/components/Layout.tsx` | Keyboard shortcut handler (Cmd+K) |
|
||||
| `src/hooks/useSearchHighlighting.ts` | Result highlighting and scroll-to-match |
|
||||
|
||||
### Adding search to new tables
|
||||
|
||||
To add search to a new table:
|
||||
|
||||
1. Add a search index in `convex/schema.ts`:
|
||||
|
||||
```typescript
|
||||
myTable: defineTable({
|
||||
title: v.string(),
|
||||
body: v.string(),
|
||||
status: v.string(),
|
||||
})
|
||||
.searchIndex("search_body", {
|
||||
searchField: "body",
|
||||
filterFields: ["status"],
|
||||
}),
|
||||
```
|
||||
|
||||
2. Create a search query in a Convex function:
|
||||
|
||||
```typescript
|
||||
const results = await ctx.db
|
||||
.query("myTable")
|
||||
.withSearchIndex("search_body", (q) =>
|
||||
q.search("body", searchTerm).eq("status", "published")
|
||||
)
|
||||
.take(10);
|
||||
```
|
||||
|
||||
3. Run `npx convex dev` to deploy the new index
|
||||
|
||||
### Limitations
|
||||
|
||||
- Search indexes only work on string fields
|
||||
- One `searchField` per index (create multiple indexes for multiple fields)
|
||||
- Filter fields support equality only (not ranges or inequalities)
|
||||
- Results are ranked by relevance, not by date or other fields
|
||||
- Maximum 10 results per `.take()` call (paginate for more)
|
||||
|
||||
### Resources
|
||||
|
||||
- [Convex Full-Text Search](https://docs.convex.dev/search/text-search)
|
||||
- [Search Index API](https://docs.convex.dev/api/classes/server.TableDefinition#searchindex)
|
||||
- [Semantic Search](/docs-semantic-search) - Vector-based search for finding similar content
|
||||
134
content/pages/docs-semantic-search.md
Normal file
134
content/pages/docs-semantic-search.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: "Semantic Search"
|
||||
slug: "docs-semantic-search"
|
||||
published: true
|
||||
order: 2
|
||||
showInNav: false
|
||||
layout: "sidebar"
|
||||
rightSidebar: true
|
||||
showFooter: true
|
||||
docsSection: true
|
||||
docsSectionOrder: 4
|
||||
docsSectionGroup: "Setup"
|
||||
docsSectionGroupIcon: "Rocket"
|
||||
---
|
||||
|
||||
## Semantic Search
|
||||
|
||||
Semantic search finds content by meaning, not exact words. Ask questions naturally and find conceptually related content.
|
||||
|
||||
Press `Cmd+K` then `Tab` to switch to Semantic mode. For exact word matching, see [Keyword Search](/docs-search).
|
||||
|
||||
---
|
||||
|
||||
### When to use each mode
|
||||
|
||||
| Use case | Mode |
|
||||
|----------|------|
|
||||
| "authentication error" (exact term) | Keyword |
|
||||
| "login problems" (conceptual) | Semantic |
|
||||
| Find specific code or commands | Keyword |
|
||||
| "how do I deploy?" (question) | Semantic |
|
||||
| Need matches highlighted on page | Keyword |
|
||||
| Not sure of exact terminology | Semantic |
|
||||
|
||||
### How semantic search works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ SEMANTIC SEARCH FLOW │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐
|
||||
│ User query: │───▶│ OpenAI API │───▶│ Query embedding │
|
||||
│ "how to │ │ text-embedding- │ │ [0.12, -0.45, │
|
||||
│ deploy" │ │ ada-002 │ │ 0.78, ...] │
|
||||
└──────────────┘ └─────────────────┘ └────────┬─────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Convex vectorSearch │
|
||||
│ Compare to stored │
|
||||
│ post/page embeddings│
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ Results sorted by │
|
||||
│ similarity score │
|
||||
│ (0-100%) │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
1. Your query is converted to a vector (1536 numbers) using OpenAI's embedding model
|
||||
2. Convex compares this vector to stored embeddings for all posts and pages
|
||||
3. Results are ranked by similarity score (higher = more similar meaning)
|
||||
4. Top 15 results returned
|
||||
|
||||
### Technical comparison
|
||||
|
||||
| Aspect | Keyword | Semantic |
|
||||
|--------|---------|----------|
|
||||
| Speed | Instant | ~300ms |
|
||||
| Cost | Free | ~$0.0001/query |
|
||||
| Highlighting | Yes | No |
|
||||
| API required | No | OpenAI |
|
||||
|
||||
### Configuration
|
||||
|
||||
Semantic search requires an OpenAI API key:
|
||||
|
||||
```bash
|
||||
npx convex env set OPENAI_API_KEY sk-your-key-here
|
||||
```
|
||||
|
||||
If the key is not configured:
|
||||
- Semantic search returns empty results
|
||||
- Keyword search continues to work normally
|
||||
- Sync script skips embedding generation
|
||||
|
||||
### How embeddings are generated
|
||||
|
||||
When you run `npm run sync`:
|
||||
|
||||
1. Content syncs to Convex (posts and pages)
|
||||
2. Script checks for posts/pages without embeddings
|
||||
3. For each, combines title + content into text
|
||||
4. Calls OpenAI to generate 1536-dimension embedding
|
||||
5. Stores embedding in Convex database
|
||||
|
||||
Embeddings are generated once per post/page. If content changes, a new embedding is generated on the next sync.
|
||||
|
||||
### Files involved
|
||||
|
||||
| File | Purpose |
|
||||
| ---- | ------- |
|
||||
| `convex/schema.ts` | `embedding` field and `vectorIndex` on posts/pages |
|
||||
| `convex/embeddings.ts` | Embedding generation actions |
|
||||
| `convex/embeddingsQueries.ts` | Queries for posts/pages without embeddings |
|
||||
| `convex/semanticSearch.ts` | Vector search action |
|
||||
| `convex/semanticSearchQueries.ts` | Queries for hydrating search results |
|
||||
| `src/components/SearchModal.tsx` | Mode toggle (Tab to switch) |
|
||||
| `scripts/sync-posts.ts` | Triggers embedding generation after sync |
|
||||
|
||||
### Limitations
|
||||
|
||||
- **No highlighting**: Semantic search finds meaning, not exact words, so matches can't be highlighted
|
||||
- **API cost**: Each search query costs ~$0.0001 (embedding generation)
|
||||
- **Latency**: ~300ms vs instant for keyword search (API round-trip)
|
||||
- **Requires OpenAI key**: Won't work without `OPENAI_API_KEY` configured
|
||||
- **Token limit**: Content is truncated to ~8000 characters for embedding
|
||||
|
||||
### Similarity scores
|
||||
|
||||
Results show a percentage score (0-100%):
|
||||
- **90%+**: Very similar meaning
|
||||
- **70-90%**: Related content
|
||||
- **50-70%**: Loosely related
|
||||
- **<50%**: Weak match (may not be relevant)
|
||||
|
||||
### Resources
|
||||
|
||||
- [Convex Vector Search](https://docs.convex.dev/search/vector-search)
|
||||
- [OpenAI Embeddings](https://platform.openai.com/docs/guides/embeddings)
|
||||
- [Keyword Search](/docs-search) - Full-text search documentation
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,3 +30,5 @@ agents. -->
|
||||
**Real-time team sync** — Multiple developers run npm run sync from different machines.
|
||||
|
||||
**Sync Commands** - Sync discovery commands to update AGENTS.md, CLAUDE.md, and llms.txt
|
||||
|
||||
**Semantic search** - Find content by meaning, not just keywords, using vector embeddings.
|
||||
|
||||
Reference in New Issue
Block a user