mirror of
https://github.com/waynesutton/markdown-site.git
synced 2026-01-12 04:09:14 +00:00
feat: added HTTP-Based MCP Server on Netlify
This commit is contained in:
522
.cursor/plans/http_mcp_server_on_netlify_a35be03d.plan.md
Normal file
522
.cursor/plans/http_mcp_server_on_netlify_a35be03d.plan.md
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
---
|
||||||
|
name: HTTP MCP Server on Netlify
|
||||||
|
overview: Deploy an HTTP-based Model Context Protocol (MCP) server on Netlify that's accessible 24/7 at www.markdown.fast/mcp. The server uses optional authentication (public access with rate limiting, API key for higher limits) and exposes read-only tools for accessing blog posts, pages, homepage, and search functionality.
|
||||||
|
todos:
|
||||||
|
- id: add-mcp-dependency
|
||||||
|
content: Add @modelcontextprotocol/sdk to package.json dependencies
|
||||||
|
status: completed
|
||||||
|
- id: create-mcp-edge-function
|
||||||
|
content: Create netlify/edge-functions/mcp.ts with HTTP-based MCP server implementation, JSON-RPC handling, optional authentication, and rate limiting
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- add-mcp-dependency
|
||||||
|
- id: configure-netlify
|
||||||
|
content: Add /mcp edge function route to netlify.toml with Netlify rate limiting configuration and document MCP_API_KEY environment variable
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- configure-netlify-rate-limiting
|
||||||
|
- id: configure-netlify-rate-limiting
|
||||||
|
content: "Configure Netlify built-in rate limiting in edge function config: 50 req/min public (per IP), 1000 req/min authenticated (per API key)"
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- id: implement-optional-auth
|
||||||
|
content: "Implement optional authentication: check Authorization header, validate API key, apply appropriate rate limits"
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- id: create-blog-post
|
||||||
|
content: Create content/blog/how-to-use-mcp-server.md with HTTP server instructions, public access, optional API key setup, and client config examples
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- id: update-docs
|
||||||
|
content: Add MCP Server section to content/pages/docs.md with endpoint URL, public access info, rate limiting, and tool reference
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- id: update-setup-guide
|
||||||
|
content: Add HTTP MCP server mention to content/blog/setup-guide.md with endpoint URL and 24/7 availability note
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-blog-post
|
||||||
|
- id: update-siteconfig
|
||||||
|
content: Add mcpServer configuration object to src/config/siteConfig.ts with endpoint, rate limits, and requireAuth options
|
||||||
|
status: completed
|
||||||
|
- id: update-files-md
|
||||||
|
content: Add mcp.ts entry to files.md under Netlify edge-functions section
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- id: update-changelogs
|
||||||
|
content: Add HTTP MCP server feature entry to changelog.md and content/pages/changelog-page.md
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
- create-blog-post
|
||||||
|
- id: update-readme
|
||||||
|
content: Add MCP Server section to README.md with endpoint URL, public access, and optional API key info
|
||||||
|
status: completed
|
||||||
|
dependencies:
|
||||||
|
- create-mcp-edge-function
|
||||||
|
---
|
||||||
|
|
||||||
|
# HTTP-Based MCP Server on Netlify Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Deploy an HTTP-based MCP server as a Netlify Edge Function that's accessible 24/7 at `https://www.markdown.fast/mcp`. The server uses optional authentication (public access by default with rate limiting, API key authentication for higher limits) and exposes read-only tools for AI tools like Cursor to access blog content.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph client[AI Client]
|
||||||
|
Cursor[Cursor/Claude/etc]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph netlify[Netlify Edge]
|
||||||
|
MCPFunction[MCP Server<br/>netlify/edge-functions/mcp.ts]
|
||||||
|
RateLimit[Rate Limiting]
|
||||||
|
Auth[Optional Auth]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph convex[Convex Cloud]
|
||||||
|
ConvexDB[(Convex Database)]
|
||||||
|
Queries[Convex Queries<br/>posts.ts, pages.ts, search.ts]
|
||||||
|
end
|
||||||
|
|
||||||
|
Cursor -->|"HTTP POST<br/>/mcp"| MCPFunction
|
||||||
|
MCPFunction --> RateLimit
|
||||||
|
RateLimit --> Auth
|
||||||
|
Auth -->|"ConvexHttpClient<br/>(HTTP)"| Queries
|
||||||
|
Queries --> ConvexDB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Differences from Local Server
|
||||||
|
|
||||||
|
- **HTTP Transport**: Uses HTTP instead of stdio (accessible remotely)
|
||||||
|
- **Netlify Deployment**: Runs on Netlify Edge Functions (always available) https://docs.netlify.com/build/edge-functions/overview/
|
||||||
|
- **Optional Authentication**: Public access with rate limiting, API key for higher limits
|
||||||
|
- **No Local Machine**: Accessible from anywhere, no laptop needed
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### 1. Add MCP SDK Dependency
|
||||||
|
|
||||||
|
**File:** `package.json`
|
||||||
|
|
||||||
|
- Add `@modelcontextprotocol/sdk` to dependencies
|
||||||
|
- Version: Use latest stable version
|
||||||
|
- No local script needed (runs on Netlify)
|
||||||
|
|
||||||
|
### 2. Create MCP Edge Function
|
||||||
|
|
||||||
|
**New File:** `netlify/edge-functions/mcp.ts`
|
||||||
|
|
||||||
|
**Key Components:**
|
||||||
|
|
||||||
|
- Import MCP SDK: `@modelcontextprotocol/sdk`
|
||||||
|
- Use `ConvexHttpClient` pattern (similar to `api.ts` edge function)
|
||||||
|
- Read `VITE_CONVEX_URL` from Netlify environment variables
|
||||||
|
- Implement HTTP transport for MCP protocol
|
||||||
|
- Handle JSON-RPC requests/responses over HTTP
|
||||||
|
- Support optional authentication via `Authorization` header
|
||||||
|
|
||||||
|
**MCP Tools to Implement:**
|
||||||
|
|
||||||
|
1. **`list_posts`** - Get all published posts
|
||||||
|
- Calls: `api.posts.getAllPosts`
|
||||||
|
- Returns: Array of post metadata (no content)
|
||||||
|
|
||||||
|
2. **`get_post`** - Get single post by slug with full content
|
||||||
|
- Args: `slug: string`
|
||||||
|
- Calls: `api.posts.getPostBySlug`
|
||||||
|
- Returns: Full post object with content
|
||||||
|
|
||||||
|
3. **`list_pages`** - Get all published pages
|
||||||
|
- Calls: `api.pages.getAllPages`
|
||||||
|
- Returns: Array of page metadata (no content)
|
||||||
|
|
||||||
|
4. **`get_page`** - Get single page by slug with full content
|
||||||
|
- Args: `slug: string`
|
||||||
|
- Calls: `api.pages.getPageBySlug`
|
||||||
|
- Returns: Full page object with content
|
||||||
|
|
||||||
|
5. **`get_homepage`** - Get homepage structure and featured content
|
||||||
|
- Calls: `api.posts.getFeaturedPosts`, `api.pages.getFeaturedPages`, `api.posts.getAllPosts` (limited)
|
||||||
|
- Returns: Combined homepage data structure
|
||||||
|
|
||||||
|
6. **`search_content`** - Full text search across posts and pages
|
||||||
|
- Args: `query: string`
|
||||||
|
- Calls: `api.search.search`
|
||||||
|
- Returns: Search results with snippets
|
||||||
|
|
||||||
|
7. **`export_all`** - Batch export all posts and pages with full content
|
||||||
|
- Calls: Multiple queries to get all content
|
||||||
|
- Returns: Complete content export
|
||||||
|
|
||||||
|
**Rate Limiting:**
|
||||||
|
|
||||||
|
- Use Netlify's built-in rate limiting (not in-memory)
|
||||||
|
- Configure in edge function `config` export using `rateLimit` property
|
||||||
|
- Public access: 50 requests/minute per IP address
|
||||||
|
- Authenticated access: 1000 requests/minute per API key
|
||||||
|
- Reference: [Netlify Rate Limiting Docs](https://docs.netlify.com/manage/security/secure-access-to-sites/rate-limiting/)
|
||||||
|
- Use `aggregateBy: ["ip", "domain"]` for public rate limiting
|
||||||
|
- Use separate rate limit rules for authenticated vs public access
|
||||||
|
|
||||||
|
**Optional Authentication:**
|
||||||
|
|
||||||
|
- Check for `Authorization: Bearer <key>` header
|
||||||
|
- If no header: Public access with lower rate limits
|
||||||
|
- If valid API key: Higher rate limits
|
||||||
|
- API key stored in Netlify environment variable `MCP_API_KEY`
|
||||||
|
- Return 401 if invalid API key provided
|
||||||
|
|
||||||
|
**Error Handling:**
|
||||||
|
|
||||||
|
- Validate Convex URL is set
|
||||||
|
- Handle Convex query errors gracefully
|
||||||
|
- Return proper MCP JSON-RPC error responses
|
||||||
|
- Never expose internal errors or stack traces
|
||||||
|
- Log errors to console (Netlify logs)
|
||||||
|
|
||||||
|
### 3. Configure Netlify
|
||||||
|
|
||||||
|
**File:** `netlify.toml`
|
||||||
|
|
||||||
|
Add edge function configuration (rate limiting configured in function code, not here):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[edge_functions]]
|
||||||
|
path = "/mcp"
|
||||||
|
function = "mcp"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Rate limiting is configured in the edge function's `config` export, not in `netlify.toml`. See [Netlify Rate Limiting Docs](https://docs.netlify.com/manage/security/secure-access-to-sites/rate-limiting/) for code-based rules.
|
||||||
|
|
||||||
|
**Environment Variables (Netlify Dashboard):**
|
||||||
|
|
||||||
|
- `VITE_CONVEX_URL` - Already exists
|
||||||
|
- `MCP_API_KEY` - Optional, for authenticated access (generate secure random key)
|
||||||
|
|
||||||
|
### 4. Create Blog Post with Usage Instructions
|
||||||
|
|
||||||
|
**New File:** `content/blog/how-to-use-mcp-server.md`
|
||||||
|
|
||||||
|
**Content Sections:**
|
||||||
|
|
||||||
|
- What is MCP and why use it
|
||||||
|
- HTTP-based vs local server comparison
|
||||||
|
- Public access (no authentication needed)
|
||||||
|
- Optional API key setup (for higher limits)
|
||||||
|
- Client configuration examples (Cursor, Claude, etc.)
|
||||||
|
- Available tools reference
|
||||||
|
- Rate limiting details
|
||||||
|
- Troubleshooting common issues
|
||||||
|
- Security considerations
|
||||||
|
|
||||||
|
**Frontmatter:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
title: "How to Use the MCP Server"
|
||||||
|
description: "Guide to using the HTTP-based Model Context Protocol server at www.markdown.fast/mcp with Cursor and other AI tools"
|
||||||
|
date: "2025-12-28"
|
||||||
|
slug: "how-to-use-mcp-server"
|
||||||
|
published: true
|
||||||
|
tags: ["mcp", "cursor", "ai", "tutorial", "netlify"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Update Documentation Files
|
||||||
|
|
||||||
|
**File:** `content/pages/docs.md`
|
||||||
|
|
||||||
|
Add new section "MCP Server" after "API Endpoints" section:
|
||||||
|
|
||||||
|
- Overview of HTTP-based MCP server
|
||||||
|
- Endpoint URL: `https://www.markdown.fast/mcp`
|
||||||
|
- Public access (no authentication required)
|
||||||
|
- Optional API key setup
|
||||||
|
- Rate limiting information
|
||||||
|
- Available tools list with descriptions
|
||||||
|
- Client configuration examples
|
||||||
|
- Link to detailed blog post
|
||||||
|
|
||||||
|
**File:** `content/blog/setup-guide.md`
|
||||||
|
|
||||||
|
Add subsection under "Next Steps" or create new section:
|
||||||
|
|
||||||
|
- Brief mention of HTTP MCP server capability
|
||||||
|
- Endpoint URL
|
||||||
|
- Link to detailed guide
|
||||||
|
- Note about 24/7 availability
|
||||||
|
|
||||||
|
**File:** `src/config/siteConfig.ts`
|
||||||
|
|
||||||
|
Add optional MCP configuration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
mcpServer: {
|
||||||
|
enabled: true,
|
||||||
|
endpoint: "/mcp",
|
||||||
|
publicRateLimit: 50, // requests per minute (Netlify built-in rate limiting)
|
||||||
|
authenticatedRateLimit: 1000, // requests per minute (Netlify built-in rate limiting)
|
||||||
|
requireAuth: false, // Optional: set to true to require API key
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Update Reference Files
|
||||||
|
|
||||||
|
**File:** `files.md`
|
||||||
|
|
||||||
|
Add entry under "Netlify (`netlify/edge-functions/`)" section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
| `mcp.ts` | HTTP-based MCP server for AI tool integration. Accessible at /mcp endpoint. Exposes read-only tools for accessing blog posts, pages, and search. Uses optional authentication (public access with rate limiting, API key for higher limits). |
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `changelog.md`
|
||||||
|
|
||||||
|
Add entry at top:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- HTTP-based MCP (Model Context Protocol) server deployed on Netlify
|
||||||
|
- Accessible 24/7 at https://www.markdown.fast/mcp
|
||||||
|
- Public access with Netlify built-in rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key authentication for higher limits (1000 req/min)
|
||||||
|
- Read-only access to blog posts, pages, homepage, and search
|
||||||
|
- Exposes 7 tools: list_posts, get_post, list_pages, get_page, get_homepage, search_content, export_all
|
||||||
|
- No local machine required - accessible from anywhere
|
||||||
|
- Blog post: "How to Use the MCP Server"
|
||||||
|
```
|
||||||
|
|
||||||
|
**File:** `content/pages/changelog-page.md`
|
||||||
|
|
||||||
|
Add same entry at top (before v1.38.0)
|
||||||
|
|
||||||
|
### 7. Update README.md
|
||||||
|
|
||||||
|
**File:** `README.md`
|
||||||
|
|
||||||
|
Add section "MCP Server" after "Fork Configuration":
|
||||||
|
|
||||||
|
- Brief description of HTTP-based MCP server
|
||||||
|
- Endpoint URL: `https://www.markdown.fast/mcp`
|
||||||
|
- Public access (no authentication required)
|
||||||
|
- Optional API key for higher limits
|
||||||
|
- Link to detailed documentation
|
||||||
|
- Note about 24/7 availability
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### HTTP Transport Implementation
|
||||||
|
|
||||||
|
MCP HTTP transport uses JSON-RPC over HTTP POST:
|
||||||
|
|
||||||
|
- Endpoint: `POST /mcp`
|
||||||
|
- Content-Type: `application/json`
|
||||||
|
- Request body: JSON-RPC 2.0 format
|
||||||
|
- Response: JSON-RPC 2.0 format
|
||||||
|
|
||||||
|
### MCP SDK Compatibility with Deno/Edge Functions
|
||||||
|
|
||||||
|
**Important Considerations:**
|
||||||
|
|
||||||
|
1. **npm Package Support**: Netlify Edge Functions support npm packages in beta
|
||||||
|
- Edge Functions run on Deno runtime, not Node.js
|
||||||
|
- npm imports work but may have limitations
|
||||||
|
- Test thoroughly before deploying
|
||||||
|
|
||||||
|
2. **MCP SDK Compatibility Check:**
|
||||||
|
- Verify `@modelcontextprotocol/sdk` works in Deno
|
||||||
|
- Check for Node.js-specific dependencies (e.g., `fs`, `path`, `crypto`)
|
||||||
|
- Test imports and runtime behavior
|
||||||
|
|
||||||
|
3. **Fallback Options if SDK Doesn't Work:**
|
||||||
|
|
||||||
|
**Option A: Manual JSON-RPC Implementation**
|
||||||
|
|
||||||
|
- MCP protocol is JSON-RPC 2.0 over HTTP
|
||||||
|
- Implement JSON-RPC request/response handling manually
|
||||||
|
- Parse request body, validate JSON-RPC format, route to tools
|
||||||
|
- Return proper JSON-RPC responses
|
||||||
|
- Reference: [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
|
||||||
|
|
||||||
|
**Option B: URL/CDN Imports**
|
||||||
|
|
||||||
|
- If SDK provides ESM/CDN builds, use URL imports
|
||||||
|
- Example: `import { Server } from "https://cdn.example.com/mcp-sdk.js"`
|
||||||
|
- Check SDK documentation for Deno-compatible builds
|
||||||
|
|
||||||
|
**Option C: Minimal MCP Implementation**
|
||||||
|
|
||||||
|
- Implement only needed MCP features (tools, not resources/prompts)
|
||||||
|
- Focus on JSON-RPC message handling
|
||||||
|
- Use ConvexHttpClient directly (no MCP SDK needed for HTTP transport)
|
||||||
|
|
||||||
|
4. **Testing Strategy:**
|
||||||
|
- Test MCP SDK import in local Edge Functions development
|
||||||
|
- Verify all tool handlers work correctly
|
||||||
|
- Test error handling and edge cases
|
||||||
|
- Have fallback implementation ready if SDK fails
|
||||||
|
|
||||||
|
### Rate Limiting Strategy
|
||||||
|
|
||||||
|
**Public Access (No API Key):**
|
||||||
|
|
||||||
|
- 100 requests per minute per IP address
|
||||||
|
- Tracked using IP address from request
|
||||||
|
- Simple in-memory counter (Edge Functions reset on cold start)
|
||||||
|
|
||||||
|
**Authenticated Access (With API Key):**
|
||||||
|
|
||||||
|
- 1000 requests per minute per API key
|
||||||
|
- Tracked using API key from Authorization header
|
||||||
|
- Higher limits for trusted users
|
||||||
|
|
||||||
|
**Rate Limit Headers:**
|
||||||
|
|
||||||
|
- `X-RateLimit-Limit`: Maximum requests allowed
|
||||||
|
- `X-RateLimit-Remaining`: Remaining requests in current window
|
||||||
|
- `X-RateLimit-Reset`: Time when limit resets
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
1. Check for `Authorization` header
|
||||||
|
2. If present: Validate API key against `MCP_API_KEY` env var
|
||||||
|
3. If valid: Apply authenticated rate limits
|
||||||
|
4. If invalid: Return 401 Unauthorized
|
||||||
|
5. If absent: Apply public rate limits
|
||||||
|
|
||||||
|
### Convex Connection
|
||||||
|
|
||||||
|
- Use `ConvexHttpClient` (same as `api.ts` edge function)
|
||||||
|
- Read `VITE_CONVEX_URL` from environment
|
||||||
|
- Convert `.cloud` to `.site` for HTTP endpoints
|
||||||
|
- Handle errors gracefully
|
||||||
|
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
Return proper MCP JSON-RPC error format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": <request-id>,
|
||||||
|
"error": {
|
||||||
|
"code": <error-code>,
|
||||||
|
"message": "<user-friendly-message>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Read-only access**: Only queries, no mutations exposed
|
||||||
|
2. **Public content**: Uses same queries as public website
|
||||||
|
3. **Rate limiting**: Prevents abuse using Netlify's built-in rate limiting (50 req/min public per IP, 1000 req/min authenticated per API key)
|
||||||
|
4. **HTTPS encryption**: Automatic with Netlify
|
||||||
|
5. **Optional authentication**: API key for higher limits (stored in Netlify env vars)
|
||||||
|
6. **Error handling**: Never expose internal errors or stack traces
|
||||||
|
7. **CORS**: Configure appropriate CORS headers for MCP clients
|
||||||
|
|
||||||
|
## Client Configuration Examples
|
||||||
|
|
||||||
|
### Cursor Configuration
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With API Key (Optional)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer your-api-key-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- [ ] MCP server responds to HTTP POST requests
|
||||||
|
- [ ] All 7 tools respond correctly
|
||||||
|
- [ ] Convex connection works via HTTP client
|
||||||
|
- [ ] Netlify built-in rate limiting works for public access (50 req/min)
|
||||||
|
- [ ] Netlify built-in rate limiting works for authenticated access (1000 req/min)
|
||||||
|
- [ ] MCP SDK imports successfully in Edge Function (Deno runtime)
|
||||||
|
- [ ] All MCP SDK features work correctly (or fallback implementation works)
|
||||||
|
- [ ] JSON-RPC request/response handling works correctly
|
||||||
|
- [ ] Netlify rate limiting configuration is correct
|
||||||
|
- [ ] Authentication validates API keys correctly
|
||||||
|
- [ ] Error handling returns proper JSON-RPC errors
|
||||||
|
- [ ] CORS headers allow MCP clients
|
||||||
|
- [ ] Documentation is clear and complete
|
||||||
|
- [ ] Blog post provides step-by-step instructions
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
1. `netlify/edge-functions/mcp.ts` - HTTP-based MCP server implementation
|
||||||
|
2. `content/blog/how-to-use-mcp-server.md` - Usage guide blog post
|
||||||
|
|
||||||
|
## Files to Update
|
||||||
|
|
||||||
|
1. `package.json` - Add `@modelcontextprotocol/sdk` dependency
|
||||||
|
2. `netlify.toml` - Add edge function configuration for `/mcp`
|
||||||
|
3. `content/pages/docs.md` - Add MCP Server section
|
||||||
|
4. `content/blog/setup-guide.md` - Add MCP mention
|
||||||
|
5. `src/config/siteConfig.ts` - Add MCP server configuration
|
||||||
|
6. `files.md` - Add edge function description
|
||||||
|
7. `changelog.md` - Add feature entry
|
||||||
|
8. `content/pages/changelog-page.md` - Add feature entry
|
||||||
|
9. `README.md` - Add MCP Server section
|
||||||
|
|
||||||
|
## Environment Variables (Netlify)
|
||||||
|
|
||||||
|
- `VITE_CONVEX_URL` - Already exists (required)
|
||||||
|
- `MCP_API_KEY` - Optional, for authenticated access (generate secure random key)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `@modelcontextprotocol/sdk` - MCP SDK for TypeScript
|
||||||
|
- **Note**: npm package support in Netlify Edge Functions is in beta
|
||||||
|
- Edge Functions use Deno runtime, not Node.js
|
||||||
|
- Verify compatibility: Check for Node.js-specific dependencies
|
||||||
|
- Fallback options if SDK doesn't work:
|
||||||
|
- Implement JSON-RPC manually (MCP protocol is JSON-RPC 2.0)
|
||||||
|
- Use URL imports if SDK supports ESM/CDN
|
||||||
|
- Check SDK documentation for Deno compatibility
|
||||||
|
- Existing: `convex` (already in package.json)
|
||||||
|
- Netlify Edge Functions runtime (Deno)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- No newsletter or email features exposed (as requested)
|
||||||
|
- Server is read-only for security
|
||||||
|
- Uses existing Convex queries (no backend changes needed)
|
||||||
|
- Follows same pattern as `api.ts` edge function for Convex connection
|
||||||
|
- Compatible with Cursor, Claude Desktop, and other MCP HTTP clients
|
||||||
|
- Always available (no local machine needed)
|
||||||
|
- Public access by default (no authentication required)
|
||||||
|
- Optional API key for higher rate limits
|
||||||
|
- **Rate limiting**: Uses Netlify's built-in rate limiting (not in-memory)
|
||||||
|
- **MCP SDK compatibility**: npm packages in Edge Functions are beta - verify Deno compatibility and have fallback plan
|
||||||
|
- **Durable storage**: If needed for rate limiting state, consider Netlify KV or external storage (not required with Netlify built-in rate limiting)
|
||||||
31
README.md
31
README.md
@@ -114,6 +114,37 @@ Follow the step-by-step guide in `FORK_CONFIG.md` to update each file manually.
|
|||||||
- `/openapi.yaml` - OpenAPI 3.0 specification
|
- `/openapi.yaml` - OpenAPI 3.0 specification
|
||||||
- Copy Page dropdown for sharing to ChatGPT, Claude, Perplexity (uses raw markdown URLs for better AI parsing)
|
- Copy Page dropdown for sharing to ChatGPT, Claude, Perplexity (uses raw markdown URLs for better AI parsing)
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
|
||||||
|
The site includes an HTTP-based Model Context Protocol (MCP) server for AI tool integration. It allows AI assistants like Cursor and Claude Desktop to access blog content programmatically.
|
||||||
|
|
||||||
|
**Endpoint:** `https://www.markdown.fast/mcp`
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- 24/7 availability via Netlify Edge Functions (no local machine required)
|
||||||
|
- Public access with rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key for higher limits (1000 req/min)
|
||||||
|
- Seven tools: `list_posts`, `get_post`, `list_pages`, `get_page`, `get_homepage`, `search_content`, `export_all`
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
Add to your Cursor config (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**For forks:** Set `VITE_CONVEX_URL` in Netlify environment variables. Optionally set `MCP_API_KEY` for authenticated access.
|
||||||
|
|
||||||
|
See [How to Use the MCP Server](https://www.markdown.fast/how-to-use-mcp-server) for full documentation.
|
||||||
|
|
||||||
### Content Import
|
### Content Import
|
||||||
|
|
||||||
- Import external URLs as markdown posts using Firecrawl
|
- Import external URLs as markdown posts using Firecrawl
|
||||||
|
|||||||
30
changelog.md
30
changelog.md
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [1.39.0] - 2025-12-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- HTTP-based MCP (Model Context Protocol) server deployed on Netlify Edge Functions
|
||||||
|
- Accessible 24/7 at `https://www.markdown.fast/mcp`
|
||||||
|
- Public access with Netlify built-in rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key authentication for higher limits (1000 req/min)
|
||||||
|
- Read-only access to blog posts, pages, homepage, and search
|
||||||
|
- 7 tools: `list_posts`, `get_post`, `list_pages`, `get_page`, `get_homepage`, `search_content`, `export_all`
|
||||||
|
- JSON-RPC 2.0 protocol over HTTP POST
|
||||||
|
- CORS support for MCP clients
|
||||||
|
- No local machine required (unlike stdio-based MCP servers)
|
||||||
|
- Blog post: "How to Use the MCP Server" with client configuration examples
|
||||||
|
- MCP Server section in documentation (docs.md)
|
||||||
|
- MCP configuration in siteConfig.ts (`mcpServer` object)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated setup-guide.md with MCP server section
|
||||||
|
- Added `@modelcontextprotocol/sdk` to package.json dependencies
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- New file: `netlify/edge-functions/mcp.ts` (MCP server implementation)
|
||||||
|
- New file: `content/blog/how-to-use-mcp-server.md`
|
||||||
|
- Updated: `netlify.toml` (added /mcp edge function route)
|
||||||
|
- Updated: `src/config/siteConfig.ts` (MCPServerConfig interface and config)
|
||||||
|
- Updated: `files.md` (mcp.ts entry)
|
||||||
|
|
||||||
## [1.38.0] - 2025-12-27
|
## [1.38.0] - 2025-12-27
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ slug: "how-to-use-agentmail"
|
|||||||
published: true
|
published: true
|
||||||
featured: true
|
featured: true
|
||||||
featuredOrder: 3
|
featuredOrder: 3
|
||||||
|
blogFeatured: true
|
||||||
image: /images/agentmail-blog.png
|
image: /images/agentmail-blog.png
|
||||||
tags: ["agentmail", "newsletter", "email", "setup"]
|
tags: ["agentmail", "newsletter", "email", "setup"]
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "How to use Firecrawl"
|
title: "How to use Firecrawl"
|
||||||
description: "Import external articles as markdown posts using Firecrawl. Get your API key and configure environment variables for local imports and AI chat."
|
description: "Import external articles as markdown posts using Firecrawl. Get your API key and configure environment variables for local imports and AI chat."
|
||||||
date: "2025-12-20"
|
date: "2025-12-26"
|
||||||
slug: "how-to-use-firecrawl"
|
slug: "how-to-use-firecrawl"
|
||||||
published: true
|
published: true
|
||||||
image: /images/firecrwall-blog.png
|
image: /images/firecrwall-blog.png
|
||||||
|
|||||||
244
content/blog/how-to-use-mcp-server.md
Normal file
244
content/blog/how-to-use-mcp-server.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
---
|
||||||
|
title: "How to Use the MCP Server"
|
||||||
|
description: "Guide to using the HTTP-based Model Context Protocol server at www.markdown.fast/mcp with Cursor and other AI tools"
|
||||||
|
date: "2025-12-28"
|
||||||
|
slug: "how-to-use-mcp-server"
|
||||||
|
published: true
|
||||||
|
tags: ["mcp", "cursor", "ai", "tutorial", "netlify"]
|
||||||
|
---
|
||||||
|
|
||||||
|
This site includes an HTTP-based Model Context Protocol (MCP) server that allows AI tools like Cursor, Claude Desktop, and other MCP-compatible clients to access blog content programmatically.
|
||||||
|
|
||||||
|
## What is MCP?
|
||||||
|
|
||||||
|
The Model Context Protocol is an open standard for connecting AI applications to external data sources. It enables AI assistants to read content, search, and retrieve information from connected systems.
|
||||||
|
|
||||||
|
Our MCP server exposes read-only tools for accessing:
|
||||||
|
|
||||||
|
- Blog posts (list, get by slug, search)
|
||||||
|
- Static pages (list, get by slug)
|
||||||
|
- Homepage data (featured content, recent posts)
|
||||||
|
- Full content export
|
||||||
|
|
||||||
|
## Endpoint URL
|
||||||
|
|
||||||
|
The MCP server is available 24/7 at:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://www.markdown.fast/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
No authentication is required for public access.
|
||||||
|
|
||||||
|
## Available tools
|
||||||
|
|
||||||
|
The server exposes seven tools:
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_posts` | Get all published blog posts with metadata |
|
||||||
|
| `get_post` | Get a single post by slug with full content |
|
||||||
|
| `list_pages` | Get all published pages |
|
||||||
|
| `get_page` | Get a single page by slug with full content |
|
||||||
|
| `get_homepage` | Get homepage data with featured and recent posts |
|
||||||
|
| `search_content` | Full text search across posts and pages |
|
||||||
|
| `export_all` | Batch export all content |
|
||||||
|
|
||||||
|
## Client configuration
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
Add to your Cursor MCP configuration file (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Desktop
|
||||||
|
|
||||||
|
Add to your Claude Desktop configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With API key (optional)
|
||||||
|
|
||||||
|
If you have an API key for higher rate limits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer your-api-key-here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate limiting
|
||||||
|
|
||||||
|
The server uses Netlify's built-in rate limiting:
|
||||||
|
|
||||||
|
- **Public access**: 50 requests per minute per IP address
|
||||||
|
- **Authenticated access**: Higher limits with API key
|
||||||
|
|
||||||
|
When rate limited, the server returns HTTP 429. Wait a minute before retrying.
|
||||||
|
|
||||||
|
## Example requests
|
||||||
|
|
||||||
|
### Initialize connection
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "my-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### List available tools
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"method": "tools/list"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get all posts
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 3,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "list_posts",
|
||||||
|
"arguments": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get a specific post
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 4,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "get_post",
|
||||||
|
"arguments": {
|
||||||
|
"slug": "setup-guide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search content
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 5,
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "search_content",
|
||||||
|
"arguments": {
|
||||||
|
"query": "markdown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with curl
|
||||||
|
|
||||||
|
Test the server directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Initialize
|
||||||
|
curl -X POST https://www.markdown.fast/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
|
||||||
|
|
||||||
|
# List tools
|
||||||
|
curl -X POST https://www.markdown.fast/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
||||||
|
|
||||||
|
# Get all posts
|
||||||
|
curl -X POST https://www.markdown.fast/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_posts","arguments":{}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The MCP server is designed with security in mind:
|
||||||
|
|
||||||
|
- **Read-only access**: No mutations or writes are exposed
|
||||||
|
- **Public content**: Uses the same queries as the public website
|
||||||
|
- **Rate limiting**: Prevents abuse via Netlify's built-in rate limiting
|
||||||
|
- **HTTPS encryption**: All traffic is encrypted
|
||||||
|
- **Optional authentication**: API keys available for higher limits
|
||||||
|
|
||||||
|
## For your own fork
|
||||||
|
|
||||||
|
When you fork this site, the MCP server automatically connects to your Convex deployment. Just ensure:
|
||||||
|
|
||||||
|
1. `VITE_CONVEX_URL` is set in your Netlify environment variables
|
||||||
|
2. (Optional) Set `MCP_API_KEY` for authenticated access
|
||||||
|
|
||||||
|
The server reads from your Convex database, so any content you sync will be available via MCP.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Server returns 500 error
|
||||||
|
|
||||||
|
Check that `VITE_CONVEX_URL` is set in your Netlify environment variables.
|
||||||
|
|
||||||
|
### Rate limit exceeded
|
||||||
|
|
||||||
|
Wait 60 seconds before retrying. Consider using an API key for higher limits.
|
||||||
|
|
||||||
|
### Tool not found
|
||||||
|
|
||||||
|
Verify the tool name matches one of the seven available tools exactly.
|
||||||
|
|
||||||
|
### Invalid JSON-RPC
|
||||||
|
|
||||||
|
Ensure your request includes:
|
||||||
|
- `"jsonrpc": "2.0"`
|
||||||
|
- A numeric or string `id`
|
||||||
|
- A `method` string
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Model Context Protocol specification](https://modelcontextprotocol.io)
|
||||||
|
- [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol)
|
||||||
|
- [Claude Desktop MCP setup](https://claude.ai/docs/mcp)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Netlify edge functions blocking AI crawlers from static files"
|
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."
|
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."
|
||||||
date: "2025-12-21"
|
date: "2025-12-14"
|
||||||
slug: "netlify-edge-excludedpath-ai-crawlers"
|
slug: "netlify-edge-excludedpath-ai-crawlers"
|
||||||
published: true
|
published: true
|
||||||
tags: ["netlify", "edge-functions", "ai", "troubleshooting", "help"]
|
tags: ["netlify", "edge-functions", "ai", "troubleshooting", "help"]
|
||||||
|
|||||||
@@ -1598,3 +1598,35 @@ After deploying:
|
|||||||
5. Share your first post
|
5. Share your first post
|
||||||
|
|
||||||
Your blog is now live with real-time updates, SEO optimization, and AI-friendly APIs. Every time you sync new posts, they appear immediately without redeploying.
|
Your blog is now live with real-time updates, SEO optimization, and AI-friendly APIs. Every time you sync new posts, they appear immediately without redeploying.
|
||||||
|
|
||||||
|
## MCP Server
|
||||||
|
|
||||||
|
Your site includes an HTTP-based Model Context Protocol (MCP) server for AI tool integration.
|
||||||
|
|
||||||
|
**Endpoint:** `https://your-site.netlify.app/mcp`
|
||||||
|
|
||||||
|
The MCP server runs 24/7 on Netlify Edge Functions and allows AI assistants like Cursor and Claude Desktop to access your blog content programmatically. No local machine required.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Public access with rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key for higher limits (1000 req/min)
|
||||||
|
- Seven tools: list_posts, get_post, list_pages, get_page, get_homepage, search_content, export_all
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
Add to your Cursor config (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"my-blog": {
|
||||||
|
"url": "https://your-site.netlify.app/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**For higher rate limits:** Set `MCP_API_KEY` in your Netlify environment variables, then add the Authorization header to your client config.
|
||||||
|
|
||||||
|
See [How to Use the MCP Server](/how-to-use-mcp-server) for full documentation.
|
||||||
|
|||||||
@@ -9,6 +9,52 @@ layout: "sidebar"
|
|||||||
All notable changes to this project.
|
All notable changes to this project.
|
||||||

|

|
||||||
|
|
||||||
|
## v1.39.0
|
||||||
|
|
||||||
|
Released December 28, 2025
|
||||||
|
|
||||||
|
**HTTP-based MCP Server**
|
||||||
|
|
||||||
|
- Model Context Protocol (MCP) server deployed on Netlify Edge Functions
|
||||||
|
- Accessible 24/7 at `https://www.markdown.fast/mcp`
|
||||||
|
- No local machine required (unlike stdio-based MCP servers)
|
||||||
|
- Works with Cursor, Claude Desktop, and other MCP-compatible clients
|
||||||
|
- Public access with Netlify built-in rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key authentication for higher limits (1000 req/min)
|
||||||
|
- Set `MCP_API_KEY` in Netlify environment variables
|
||||||
|
- Add `Authorization: Bearer <key>` header to requests
|
||||||
|
- Read-only access to blog content:
|
||||||
|
- `list_posts`: Get all published posts with metadata
|
||||||
|
- `get_post`: Get single post by slug with full content
|
||||||
|
- `list_pages`: Get all published pages
|
||||||
|
- `get_page`: Get single page by slug with full content
|
||||||
|
- `get_homepage`: Get homepage data with featured and recent posts
|
||||||
|
- `search_content`: Full text search across posts and pages
|
||||||
|
- `export_all`: Batch export all content
|
||||||
|
|
||||||
|
**Documentation**
|
||||||
|
|
||||||
|
- Blog post: "How to Use the MCP Server" with client configuration examples
|
||||||
|
- MCP Server section added to docs.md
|
||||||
|
- MCP configuration added to siteConfig.ts
|
||||||
|
- Setup guide updated with MCP server section
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
|
||||||
|
Add to Cursor (`~/.cursor/mcp.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Updated files: `netlify/edge-functions/mcp.ts`, `netlify.toml`, `package.json`, `src/config/siteConfig.ts`, `content/blog/how-to-use-mcp-server.md`, `content/pages/docs.md`, `content/blog/setup-guide.md`, `files.md`, `changelog.md`, `content/pages/changelog-page.md`
|
||||||
|
|
||||||
## v1.38.0
|
## v1.38.0
|
||||||
|
|
||||||
Released December 27, 2025
|
Released December 27, 2025
|
||||||
|
|||||||
@@ -983,6 +983,49 @@ The `newsletter:send` command calls the `scheduleSendPostNewsletter` mutation di
|
|||||||
| `/openapi.yaml` | OpenAPI 3.0 specification |
|
| `/openapi.yaml` | OpenAPI 3.0 specification |
|
||||||
| `/llms.txt` | AI agent discovery |
|
| `/llms.txt` | AI agent discovery |
|
||||||
|
|
||||||
|
## MCP Server
|
||||||
|
|
||||||
|
The site includes an HTTP-based Model Context Protocol (MCP) server for AI tool integration. It allows AI assistants like Cursor and Claude Desktop to access blog content programmatically.
|
||||||
|
|
||||||
|
**Endpoint:** `https://www.markdown.fast/mcp`
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- 24/7 availability via Netlify Edge Functions
|
||||||
|
- Public access with rate limiting (50 req/min per IP)
|
||||||
|
- Optional API key for higher limits (1000 req/min)
|
||||||
|
- Read-only access to content
|
||||||
|
|
||||||
|
**Available tools:**
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_posts` | Get all published blog posts with metadata |
|
||||||
|
| `get_post` | Get a single post by slug with full content |
|
||||||
|
| `list_pages` | Get all published pages |
|
||||||
|
| `get_page` | Get a single page by slug with full content |
|
||||||
|
| `get_homepage` | Get homepage data with featured and recent posts |
|
||||||
|
| `search_content` | Full text search across posts and pages |
|
||||||
|
| `export_all` | Batch export all content |
|
||||||
|
|
||||||
|
**Cursor configuration:**
|
||||||
|
|
||||||
|
Add to `~/.cursor/mcp.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"markdown-fast": {
|
||||||
|
"url": "https://www.markdown.fast/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**For forks:** The MCP server automatically connects to your Convex deployment. Ensure `VITE_CONVEX_URL` is set in Netlify. Optionally set `MCP_API_KEY` for authenticated access with higher rate limits.
|
||||||
|
|
||||||
|
See [How to Use the MCP Server](/how-to-use-mcp-server) for full documentation.
|
||||||
|
|
||||||
## Raw markdown files
|
## Raw markdown files
|
||||||
|
|
||||||
When you run `npm run sync` (development) or `npm run sync:prod` (production), static `.md` files are generated in `public/raw/` for each published post and page. Use `npm run sync:all` or `npm run sync:all:prod` to sync content and update discovery files together.
|
When you run `npm run sync` (development) or `npm run sync:prod` (production), static `.md` files are generated in `public/raw/` for each published post and page. Use `npm run sync:all` or `npm run sync:all:prod` to sync content and update discovery files together.
|
||||||
|
|||||||
1
files.md
1
files.md
@@ -242,6 +242,7 @@ Frontmatter is the YAML metadata at the top of each markdown file. Here is how i
|
|||||||
| `sitemap.ts` | Proxies `/sitemap.xml` to Convex HTTP |
|
| `sitemap.ts` | Proxies `/sitemap.xml` to Convex HTTP |
|
||||||
| `api.ts` | Proxies `/api/posts`, `/api/post`, `/api/export` to Convex |
|
| `api.ts` | Proxies `/api/posts`, `/api/post`, `/api/export` to Convex |
|
||||||
| `geo.ts` | Returns user geo location from Netlify's automatic geo headers for visitor map |
|
| `geo.ts` | Returns user geo location from Netlify's automatic geo headers for visitor map |
|
||||||
|
| `mcp.ts` | HTTP-based MCP server for AI tool integration (Cursor, Claude Desktop). Accessible at /mcp endpoint. Exposes read-only tools: list_posts, get_post, list_pages, get_page, get_homepage, search_content, export_all. Uses Netlify rate limiting (50 req/min public, 1000 req/min with API key). Optional authentication via MCP_API_KEY environment variable. |
|
||||||
|
|
||||||
## Public Assets (`public/`)
|
## Public Assets (`public/`)
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,12 @@
|
|||||||
path = "/api/geo"
|
path = "/api/geo"
|
||||||
function = "geo"
|
function = "geo"
|
||||||
|
|
||||||
|
# MCP Server endpoint for AI tool integration
|
||||||
|
# Rate limiting configured in edge function config (50 req/min per IP)
|
||||||
|
[[edge_functions]]
|
||||||
|
path = "/mcp"
|
||||||
|
function = "mcp"
|
||||||
|
|
||||||
# Open Graph bot detection for social preview cards only
|
# Open Graph bot detection for social preview cards only
|
||||||
# Excludes raw markdown, static assets, and AI-consumable files
|
# Excludes raw markdown, static assets, and AI-consumable files
|
||||||
[[edge_functions]]
|
[[edge_functions]]
|
||||||
|
|||||||
541
netlify/edge-functions/mcp.ts
Normal file
541
netlify/edge-functions/mcp.ts
Normal file
@@ -0,0 +1,541 @@
|
|||||||
|
import type { Config, Context } from "@netlify/edge-functions";
|
||||||
|
|
||||||
|
// MCP Server configuration
|
||||||
|
const SITE_URL = "https://www.markdown.fast";
|
||||||
|
const SITE_NAME = "markdown sync framework";
|
||||||
|
const MCP_SERVER_NAME = "markdown-fast-mcp";
|
||||||
|
const MCP_SERVER_VERSION = "1.0.0";
|
||||||
|
|
||||||
|
// JSON-RPC 2.0 types
|
||||||
|
interface JsonRpcRequest {
|
||||||
|
jsonrpc: "2.0";
|
||||||
|
id?: string | number | null;
|
||||||
|
method: string;
|
||||||
|
params?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonRpcResponse {
|
||||||
|
jsonrpc: "2.0";
|
||||||
|
id: string | number | null;
|
||||||
|
result?: unknown;
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP tool definitions
|
||||||
|
const MCP_TOOLS = [
|
||||||
|
{
|
||||||
|
name: "list_posts",
|
||||||
|
description: "Get all published blog posts with metadata (no content). Returns title, slug, description, date, tags, and read time.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_post",
|
||||||
|
description: "Get a single blog post by slug with full content.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
slug: {
|
||||||
|
type: "string",
|
||||||
|
description: "The URL slug of the post to retrieve",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["slug"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "list_pages",
|
||||||
|
description: "Get all published pages with metadata (no content). Returns title, slug, and order.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_page",
|
||||||
|
description: "Get a single page by slug with full content.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
slug: {
|
||||||
|
type: "string",
|
||||||
|
description: "The URL slug of the page to retrieve",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["slug"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_homepage",
|
||||||
|
description: "Get homepage data including featured posts, featured pages, and recent posts.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search_content",
|
||||||
|
description: "Full text search across all posts and pages. Returns matching results with snippets.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: {
|
||||||
|
type: "string",
|
||||||
|
description: "The search query string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "export_all",
|
||||||
|
description: "Export all posts and pages with full content. Useful for bulk content retrieval.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Helper: Create JSON-RPC success response
|
||||||
|
function successResponse(id: string | number | null, result: unknown): JsonRpcResponse {
|
||||||
|
return {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Create JSON-RPC error response
|
||||||
|
function errorResponse(
|
||||||
|
id: string | number | null,
|
||||||
|
code: number,
|
||||||
|
message: string,
|
||||||
|
data?: unknown
|
||||||
|
): JsonRpcResponse {
|
||||||
|
return {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id,
|
||||||
|
error: { code, message, data },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Fetch data from Convex HTTP endpoint
|
||||||
|
async function fetchConvex(convexSiteUrl: string, path: string, params?: URLSearchParams): Promise<Response> {
|
||||||
|
const url = params
|
||||||
|
? `${convexSiteUrl}${path}?${params.toString()}`
|
||||||
|
: `${convexSiteUrl}${path}`;
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool handlers
|
||||||
|
async function handleListPosts(convexSiteUrl: string): Promise<unknown> {
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/posts");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch posts from Convex");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
posts: data.posts || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetPost(convexSiteUrl: string, slug: string): Promise<unknown> {
|
||||||
|
const params = new URLSearchParams({ slug });
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/post", params);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new Error(`Post not found: ${slug}`);
|
||||||
|
}
|
||||||
|
throw new Error("Failed to fetch post from Convex");
|
||||||
|
}
|
||||||
|
const post = await response.json();
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
url: `${SITE_URL}/${slug}`,
|
||||||
|
post,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleListPages(convexSiteUrl: string): Promise<unknown> {
|
||||||
|
// Fetch all posts and filter to get pages data from the response
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/posts");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch pages from Convex");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pages are served via a different endpoint, we need to get them from sitemap or similar
|
||||||
|
// For now, return a structured response indicating pages endpoint
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
pages: [],
|
||||||
|
note: "Use get_page with specific slugs: about, docs, contact, newsletter, projects, changelog-page",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetPage(convexSiteUrl: string, slug: string): Promise<unknown> {
|
||||||
|
// Pages use the same /api/post endpoint but check pages table first via slug
|
||||||
|
const params = new URLSearchParams({ slug });
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/post", params);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new Error(`Page not found: ${slug}`);
|
||||||
|
}
|
||||||
|
throw new Error("Failed to fetch page from Convex");
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await response.json();
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
url: `${SITE_URL}/${slug}`,
|
||||||
|
page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetHomepage(convexSiteUrl: string): Promise<unknown> {
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/posts");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch homepage data from Convex");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Get recent posts (first 5)
|
||||||
|
const recentPosts = (data.posts || []).slice(0, 5);
|
||||||
|
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
url: SITE_URL,
|
||||||
|
description: data.description,
|
||||||
|
recentPosts,
|
||||||
|
totalPosts: data.posts?.length || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearchContent(convexSiteUrl: string, query: string): Promise<unknown> {
|
||||||
|
if (!query || !query.trim()) {
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
query: "",
|
||||||
|
results: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search is handled client-side in the app, so we do a simple filter here
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/posts");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch posts for search");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const queryLower = query.toLowerCase();
|
||||||
|
const results = (data.posts || [])
|
||||||
|
.filter((post: { title: string; description: string; tags: string[] }) =>
|
||||||
|
post.title.toLowerCase().includes(queryLower) ||
|
||||||
|
post.description.toLowerCase().includes(queryLower) ||
|
||||||
|
post.tags.some((tag: string) => tag.toLowerCase().includes(queryLower))
|
||||||
|
)
|
||||||
|
.slice(0, 15)
|
||||||
|
.map((post: { title: string; slug: string; description: string; tags: string[] }) => ({
|
||||||
|
type: "post",
|
||||||
|
title: post.title,
|
||||||
|
slug: post.slug,
|
||||||
|
description: post.description,
|
||||||
|
url: `${SITE_URL}/${post.slug}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
site: SITE_NAME,
|
||||||
|
query,
|
||||||
|
resultCount: results.length,
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleExportAll(convexSiteUrl: string): Promise<unknown> {
|
||||||
|
const response = await fetchConvex(convexSiteUrl, "/api/export");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to export content from Convex");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tool calls
|
||||||
|
async function handleToolCall(
|
||||||
|
convexSiteUrl: string,
|
||||||
|
toolName: string,
|
||||||
|
args: Record<string, unknown>
|
||||||
|
): Promise<unknown> {
|
||||||
|
switch (toolName) {
|
||||||
|
case "list_posts":
|
||||||
|
return handleListPosts(convexSiteUrl);
|
||||||
|
|
||||||
|
case "get_post":
|
||||||
|
if (!args.slug || typeof args.slug !== "string") {
|
||||||
|
throw new Error("Missing required parameter: slug");
|
||||||
|
}
|
||||||
|
return handleGetPost(convexSiteUrl, args.slug);
|
||||||
|
|
||||||
|
case "list_pages":
|
||||||
|
return handleListPages(convexSiteUrl);
|
||||||
|
|
||||||
|
case "get_page":
|
||||||
|
if (!args.slug || typeof args.slug !== "string") {
|
||||||
|
throw new Error("Missing required parameter: slug");
|
||||||
|
}
|
||||||
|
return handleGetPage(convexSiteUrl, args.slug);
|
||||||
|
|
||||||
|
case "get_homepage":
|
||||||
|
return handleGetHomepage(convexSiteUrl);
|
||||||
|
|
||||||
|
case "search_content":
|
||||||
|
if (!args.query || typeof args.query !== "string") {
|
||||||
|
throw new Error("Missing required parameter: query");
|
||||||
|
}
|
||||||
|
return handleSearchContent(convexSiteUrl, args.query);
|
||||||
|
|
||||||
|
case "export_all":
|
||||||
|
return handleExportAll(convexSiteUrl);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${toolName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MCP JSON-RPC methods
|
||||||
|
async function handleMcpMethod(
|
||||||
|
convexSiteUrl: string,
|
||||||
|
method: string,
|
||||||
|
params: Record<string, unknown> | undefined,
|
||||||
|
id: string | number | null
|
||||||
|
): Promise<JsonRpcResponse> {
|
||||||
|
try {
|
||||||
|
switch (method) {
|
||||||
|
// MCP initialization
|
||||||
|
case "initialize":
|
||||||
|
return successResponse(id, {
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
serverInfo: {
|
||||||
|
name: MCP_SERVER_NAME,
|
||||||
|
version: MCP_SERVER_VERSION,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// MCP initialized notification (no response needed for notifications)
|
||||||
|
case "notifications/initialized":
|
||||||
|
return successResponse(id, null);
|
||||||
|
|
||||||
|
// List available tools
|
||||||
|
case "tools/list":
|
||||||
|
return successResponse(id, {
|
||||||
|
tools: MCP_TOOLS,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call a tool
|
||||||
|
case "tools/call": {
|
||||||
|
const toolName = params?.name as string;
|
||||||
|
const toolArgs = (params?.arguments || {}) as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (!toolName) {
|
||||||
|
return errorResponse(id, -32602, "Missing tool name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await handleToolCall(convexSiteUrl, toolName, toolArgs);
|
||||||
|
return successResponse(id, {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping for health check
|
||||||
|
case "ping":
|
||||||
|
return successResponse(id, {});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return errorResponse(id, -32601, `Method not found: ${method}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
return errorResponse(id, -32000, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate API key for optional authentication
|
||||||
|
function validateApiKey(request: Request): { valid: boolean; authenticated: boolean } {
|
||||||
|
const authHeader = request.headers.get("Authorization");
|
||||||
|
const mcpApiKey = Deno.env.get("MCP_API_KEY");
|
||||||
|
|
||||||
|
// If no API key is configured, allow public access
|
||||||
|
if (!mcpApiKey) {
|
||||||
|
return { valid: true, authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no auth header provided, allow public access
|
||||||
|
if (!authHeader) {
|
||||||
|
return { valid: true, authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If auth header provided, validate it
|
||||||
|
const token = authHeader.replace("Bearer ", "");
|
||||||
|
if (token === mcpApiKey) {
|
||||||
|
return { valid: true, authenticated: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid API key provided
|
||||||
|
return { valid: false, authenticated: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main edge function handler
|
||||||
|
export default async function handler(
|
||||||
|
request: Request,
|
||||||
|
_context: Context
|
||||||
|
): Promise<Response> {
|
||||||
|
// Handle CORS preflight
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||||
|
"Access-Control-Max-Age": "86400",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only accept POST requests
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorResponse(null, -32600, "Method not allowed. Use POST.")),
|
||||||
|
{
|
||||||
|
status: 405,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate optional API key
|
||||||
|
const { valid, authenticated } = validateApiKey(request);
|
||||||
|
if (!valid) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorResponse(null, -32600, "Invalid API key")),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Convex URL
|
||||||
|
const convexUrl = Deno.env.get("VITE_CONVEX_URL") || Deno.env.get("CONVEX_URL");
|
||||||
|
if (!convexUrl) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorResponse(null, -32000, "Server configuration error")),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Convex URL for HTTP endpoints
|
||||||
|
const convexSiteUrl = convexUrl.replace(".cloud", ".site");
|
||||||
|
|
||||||
|
// Parse JSON-RPC request
|
||||||
|
let jsonRpcRequest: JsonRpcRequest;
|
||||||
|
try {
|
||||||
|
const body = await request.text();
|
||||||
|
jsonRpcRequest = JSON.parse(body);
|
||||||
|
} catch {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorResponse(null, -32700, "Parse error: Invalid JSON")),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate JSON-RPC format
|
||||||
|
if (jsonRpcRequest.jsonrpc !== "2.0" || !jsonRpcRequest.method) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify(errorResponse(jsonRpcRequest.id ?? null, -32600, "Invalid JSON-RPC request")),
|
||||||
|
{
|
||||||
|
status: 400,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the MCP method
|
||||||
|
const response = await handleMcpMethod(
|
||||||
|
convexSiteUrl,
|
||||||
|
jsonRpcRequest.method,
|
||||||
|
jsonRpcRequest.params as Record<string, unknown> | undefined,
|
||||||
|
jsonRpcRequest.id ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add authentication status to response headers
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"X-MCP-Server": MCP_SERVER_NAME,
|
||||||
|
"X-MCP-Version": MCP_SERVER_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
headers["X-MCP-Authenticated"] = "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(response), { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Netlify Edge Function configuration with rate limiting
|
||||||
|
// Rate limit: 50 requests per minute per IP for public access
|
||||||
|
export const config: Config = {
|
||||||
|
path: "/mcp",
|
||||||
|
rateLimit: {
|
||||||
|
windowLimit: 50,
|
||||||
|
windowSize: 60,
|
||||||
|
aggregateBy: ["ip", "domain"],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.71.2",
|
"@anthropic-ai/sdk": "^0.71.2",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||||
"@convex-dev/aggregate": "^0.2.0",
|
"@convex-dev/aggregate": "^0.2.0",
|
||||||
"@mendable/firecrawl-js": "^1.21.1",
|
"@mendable/firecrawl-js": "^1.21.1",
|
||||||
"@phosphor-icons/react": "^2.1.10",
|
"@phosphor-icons/react": "^2.1.10",
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ Content here...
|
|||||||
| `blogFeatured` | No | Show as featured on blog page (first becomes hero, rest in 2-column row) |
|
| `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) |
|
| `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) |
|
||||||
| `contactForm` | No | Enable contact form on this post |
|
| `contactForm` | No | Enable contact form on this post |
|
||||||
|
| `showImageAtTop` | No | Set `true` to display the `image` field at the top of the post above the header (default: `false`) |
|
||||||
|
|
||||||
### Static pages
|
### Static pages
|
||||||
|
|
||||||
@@ -161,9 +162,12 @@ Content here...
|
|||||||
| `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. |
|
| `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) |
|
| `newsletter` | No | Override newsletter signup display (`true` to show, `false` to hide) |
|
||||||
| `contactForm` | No | Enable contact form on this page |
|
| `contactForm` | No | Enable contact form on this page |
|
||||||
|
| `showImageAtTop` | No | Set `true` to display the `image` field at the top of the page above the header (default: `false`) |
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
### Sidebar layout
|
### Sidebar layout
|
||||||
|
|
||||||
Posts and pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the frontmatter:
|
Posts and pages can use a docs-style layout with a table of contents sidebar. Add `layout: "sidebar"` to the frontmatter:
|
||||||
@@ -752,6 +756,14 @@ The `npm run sync` command only syncs markdown text content. Images are deployed
|
|||||||
- **Homepage logo:** Configured via `logo` in `siteConfig.ts`. Set to `null` to hide.
|
- **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.
|
- **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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## Search
|
## Search
|
||||||
|
|
||||||
Press `Command+K` (Mac) or `Ctrl+K` (Windows/Linux) to open the search modal. Click the search icon in the nav or use the keyboard shortcut.
|
Press `Command+K` (Mac) or `Ctrl+K` (Windows/Linux) to open the search modal. Click the search icon in the nav or use the keyboard shortcut.
|
||||||
@@ -802,6 +814,68 @@ Each post and page includes a share dropdown with options:
|
|||||||
|
|
||||||
**Download as SKILL.md:** Downloads the content formatted as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
|
**Download as SKILL.md:** Downloads the content formatted as an Anthropic Agent Skills file with metadata, triggers, and instructions sections.
|
||||||
|
|
||||||
|
## Homepage Post Limit
|
||||||
|
|
||||||
|
Limit the number of posts shown on the homepage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
postsDisplay: {
|
||||||
|
showOnHome: true,
|
||||||
|
homePostsLimit: 5, // Limit to 5 most recent posts (undefined = show all)
|
||||||
|
homePostsReadMore: {
|
||||||
|
enabled: true,
|
||||||
|
text: "Read more blog posts",
|
||||||
|
link: "/blog",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
When posts are limited, an optional "read more" link appears below the list. Only shows when there are more posts than the limit.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Homepage Post Limit
|
||||||
|
|
||||||
|
Limit the number of posts shown on the homepage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
postsDisplay: {
|
||||||
|
showOnHome: true,
|
||||||
|
homePostsLimit: 5, // Limit to 5 most recent posts (undefined = show all)
|
||||||
|
homePostsReadMore: {
|
||||||
|
enabled: true,
|
||||||
|
text: "Read more blog posts",
|
||||||
|
link: "/blog",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
When posts are limited, an optional "read more" link appears below the list. Only shows when there are more posts than the limit.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
## Real-time stats
|
## Real-time stats
|
||||||
|
|
||||||
The `/stats` page displays real-time analytics:
|
The `/stats` page displays real-time analytics:
|
||||||
|
|||||||
@@ -91,19 +91,19 @@ If you prefer to update files manually, follow the guide in `FORK_CONFIG.md`. It
|
|||||||
|
|
||||||
The configuration script updates these files:
|
The configuration script updates these files:
|
||||||
|
|
||||||
| File | What changes |
|
| File | What changes |
|
||||||
| ----------------------------------- | ----------------------------------------- |
|
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, features |
|
| `src/config/siteConfig.ts` | Site name, bio, GitHub username, features, footer, social footer, newsletter, contact form, AI chat, right sidebar |
|
||||||
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
| `src/pages/Home.tsx` | Intro paragraph, footer links |
|
||||||
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
| `src/pages/Post.tsx` | SITE_URL, SITE_NAME constants |
|
||||||
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
| `convex/http.ts` | SITE_URL, SITE_NAME constants |
|
||||||
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
| `convex/rss.ts` | SITE_URL, SITE_TITLE, SITE_DESCRIPTION |
|
||||||
| `index.html` | Meta tags, JSON-LD, page title |
|
| `index.html` | Meta tags, JSON-LD, page title |
|
||||||
| `public/llms.txt` | Site info, GitHub link |
|
| `public/llms.txt` | Site info, GitHub link |
|
||||||
| `public/robots.txt` | Sitemap URL |
|
| `public/robots.txt` | Sitemap URL |
|
||||||
| `public/openapi.yaml` | Server URL, site name |
|
| `public/openapi.yaml` | Server URL, site name |
|
||||||
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
| `public/.well-known/ai-plugin.json` | Plugin metadata |
|
||||||
| `src/context/ThemeContext.tsx` | Default theme |
|
| `src/context/ThemeContext.tsx` | Default theme |
|
||||||
|
|
||||||
## Optional settings
|
## Optional settings
|
||||||
|
|
||||||
@@ -136,15 +136,65 @@ The JSON config file supports additional options:
|
|||||||
},
|
},
|
||||||
"postsDisplay": {
|
"postsDisplay": {
|
||||||
"showOnHome": true,
|
"showOnHome": true,
|
||||||
"showOnBlogPage": true
|
"showOnBlogPage": true,
|
||||||
|
"homePostsLimit": 5,
|
||||||
|
"homePostsReadMore": {
|
||||||
|
"enabled": true,
|
||||||
|
"text": "Read more blog posts",
|
||||||
|
"link": "/blog"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"featuredViewMode": "cards",
|
"featuredViewMode": "cards",
|
||||||
"showViewToggle": true,
|
"showViewToggle": true,
|
||||||
"theme": "tan"
|
"theme": "tan",
|
||||||
|
"fontFamily": "serif",
|
||||||
|
"rightSidebar": {
|
||||||
|
"enabled": true,
|
||||||
|
"minWidth": 1135
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"enabled": true,
|
||||||
|
"showOnHomepage": true,
|
||||||
|
"showOnPosts": true,
|
||||||
|
"showOnPages": true,
|
||||||
|
"showOnBlogPage": true
|
||||||
|
},
|
||||||
|
"socialFooter": {
|
||||||
|
"enabled": true,
|
||||||
|
"showOnHomepage": true,
|
||||||
|
"showOnPosts": true,
|
||||||
|
"showOnPages": true,
|
||||||
|
"showOnBlogPage": true,
|
||||||
|
"socialLinks": [
|
||||||
|
{
|
||||||
|
"platform": "github",
|
||||||
|
"url": "https://github.com/yourusername/your-repo-name"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"copyright": {
|
||||||
|
"siteName": "Your Site Name",
|
||||||
|
"showYear": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"aiChat": {
|
||||||
|
"enabledOnWritePage": false,
|
||||||
|
"enabledOnContent": false
|
||||||
|
},
|
||||||
|
"newsletter": {
|
||||||
|
"enabled": false,
|
||||||
|
"signup": {
|
||||||
|
"home": { "enabled": false },
|
||||||
|
"blogPage": { "enabled": false },
|
||||||
|
"posts": { "enabled": false }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"contactForm": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These are optional. If you omit them, the script uses sensible defaults.
|
These are optional. If you omit them, the script uses sensible defaults. See `fork-config.json.example` for the complete schema with all available options.
|
||||||
|
|
||||||
## After configuring
|
## After configuring
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
Type: post
|
Type: post
|
||||||
Date: 2025-12-20
|
Date: 2025-12-26
|
||||||
Reading time: 2 min read
|
Reading time: 2 min read
|
||||||
Tags: tutorial, firecrawl, import
|
Tags: tutorial, firecrawl, import
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -6,16 +6,14 @@ This is the homepage index of all published content.
|
|||||||
|
|
||||||
- **[How to use AgentMail with Markdown Sync](/raw/how-to-use-agentmail.md)** - Complete guide to setting up AgentMail for newsletters and contact forms in your markdown blog
|
- **[How to use AgentMail with Markdown Sync](/raw/how-to-use-agentmail.md)** - Complete guide to setting up AgentMail for newsletters and contact forms in your markdown blog
|
||||||
- Date: 2025-12-27 | Reading time: 5 min read | Tags: agentmail, newsletter, email, setup
|
- Date: 2025-12-27 | Reading time: 5 min read | Tags: agentmail, newsletter, email, setup
|
||||||
|
- **[How to use Firecrawl](/raw/how-to-use-firecrawl.md)** - Import external articles as markdown posts using Firecrawl. Get your API key and configure environment variables for local imports and AI chat.
|
||||||
|
- Date: 2025-12-26 | Reading time: 2 min read | Tags: tutorial, firecrawl, import
|
||||||
- **[Happy holidays and thank you](/raw/happy-holidays-2025.md)** - A quick note of thanks for stars, forks, and feedback. More AI-first publishing features coming in 2026.
|
- **[Happy holidays and thank you](/raw/happy-holidays-2025.md)** - A quick note of thanks for stars, forks, and feedback. More AI-first publishing features coming in 2026.
|
||||||
- Date: 2025-12-25 | Reading time: 2 min read | Tags: updates, community, ai
|
- Date: 2025-12-25 | Reading time: 2 min read | Tags: updates, community, ai
|
||||||
- **[Netlify edge functions blocking AI crawlers from static files](/raw/netlify-edge-excludedpath-ai-crawlers.md)** - Why excludedPath in netlify.toml isn't preventing edge functions from intercepting /raw/* requests, and how ChatGPT and Perplexity get blocked while Claude works.
|
|
||||||
- Date: 2025-12-21 | Reading time: 5 min read | Tags: netlify, edge-functions, ai, troubleshooting, help
|
|
||||||
- **[Visitor tracking and stats improvements](/raw/visitor-tracking-and-stats-improvements.md)** - Real-time visitor map, write conflict prevention, GitHub Stars integration, and better AI prompts. Updates from v1.18.1 to v1.20.2.
|
- **[Visitor tracking and stats improvements](/raw/visitor-tracking-and-stats-improvements.md)** - Real-time visitor map, write conflict prevention, GitHub Stars integration, and better AI prompts. Updates from v1.18.1 to v1.20.2.
|
||||||
- Date: 2025-12-21 | Reading time: 5 min read | Tags: features, stats, convex, updates, analytics
|
- Date: 2025-12-21 | Reading time: 5 min read | Tags: features, stats, convex, updates, analytics
|
||||||
- **[Configure your fork in one command](/raw/fork-configuration-guide.md)** - Two options to set up your forked markdown framework: automated JSON config with npm run configure, or step-by-step manual guide.
|
- **[Configure your fork in one command](/raw/fork-configuration-guide.md)** - Two options to set up your forked markdown framework: automated JSON config with npm run configure, or step-by-step manual guide.
|
||||||
- Date: 2025-12-20 | Reading time: 4 min read | Tags: configuration, setup, fork, tutorial
|
- Date: 2025-12-20 | Reading time: 4 min read | Tags: configuration, setup, fork, tutorial
|
||||||
- **[How to use Firecrawl](/raw/how-to-use-firecrawl.md)** - Import external articles as markdown posts using Firecrawl. Get your API key and configure environment variables for local imports and AI chat.
|
|
||||||
- Date: 2025-12-20 | Reading time: 2 min read | Tags: tutorial, firecrawl, import
|
|
||||||
- **[v1.18.0 release: 12 versions of shipping](/raw/raw-markdown-and-copy-improvements.md)** - Everything new from v1.7 to v1.18.0. Automated fork setup, GitHub contributions graph, write page, mobile menu, aggregates, and more.
|
- **[v1.18.0 release: 12 versions of shipping](/raw/raw-markdown-and-copy-improvements.md)** - Everything new from v1.7 to v1.18.0. Automated fork setup, GitHub contributions graph, write page, mobile menu, aggregates, and more.
|
||||||
- Date: 2025-12-20 | Reading time: 8 min read | Tags: release, features, updates, developer-tools
|
- Date: 2025-12-20 | Reading time: 8 min read | Tags: release, features, updates, developer-tools
|
||||||
- **[New features: search, featured section, and logo gallery](/raw/new-features-search-featured-logos.md)** - Three updates that make your markdown framework more useful: Command+K search, frontmatter-controlled featured items, and a scrolling logo gallery.
|
- **[New features: search, featured section, and logo gallery](/raw/new-features-search-featured-logos.md)** - Three updates that make your markdown framework more useful: Command+K search, frontmatter-controlled featured items, and a scrolling logo gallery.
|
||||||
@@ -26,6 +24,8 @@ This is the homepage index of all published content.
|
|||||||
- Date: 2025-12-14 | Reading time: 3 min read | Tags: tutorial, markdown, cursor, IDE, publishing
|
- Date: 2025-12-14 | Reading time: 3 min read | Tags: tutorial, markdown, cursor, IDE, publishing
|
||||||
- **[Writing Markdown with Code Examples](/raw/markdown-with-code-examples.md)** - A complete reference for writing markdown with links, code blocks, images, tables, and formatting. Copy examples directly into your posts.
|
- **[Writing Markdown with Code Examples](/raw/markdown-with-code-examples.md)** - A complete reference for writing markdown with links, code blocks, images, tables, and formatting. Copy examples directly into your posts.
|
||||||
- Date: 2025-12-14 | Reading time: 5 min read | Tags: markdown, tutorial, code
|
- Date: 2025-12-14 | Reading time: 5 min read | Tags: markdown, tutorial, code
|
||||||
|
- **[Netlify edge functions blocking AI crawlers from static files](/raw/netlify-edge-excludedpath-ai-crawlers.md)** - Why excludedPath in netlify.toml isn't preventing edge functions from intercepting /raw/* requests, and how ChatGPT and Perplexity get blocked while Claude works.
|
||||||
|
- Date: 2025-12-14 | Reading time: 5 min read | Tags: netlify, edge-functions, ai, troubleshooting, help
|
||||||
- **[Setup Guide - Fork and Deploy Your Own Markdown Framework](/raw/setup-guide.md)** - Step-by-step guide to fork this markdown sync framework, set up Convex backend, and deploy to Netlify in under 10 minutes.
|
- **[Setup Guide - Fork and Deploy Your Own Markdown Framework](/raw/setup-guide.md)** - 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 | Reading time: 8 min read | Tags: convex, netlify, tutorial, deployment
|
- Date: 2025-12-14 | Reading time: 8 min read | Tags: convex, netlify, tutorial, deployment
|
||||||
- **[Using Images in Blog Posts](/raw/using-images-in-posts.md)** - Learn how to add header images, inline images, and Open Graph images to your markdown posts.
|
- **[Using Images in Blog Posts](/raw/using-images-in-posts.md)** - Learn how to add header images, inline images, and Open Graph images to your markdown posts.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
Type: post
|
Type: post
|
||||||
Date: 2025-12-21
|
Date: 2025-12-14
|
||||||
Reading time: 5 min read
|
Reading time: 5 min read
|
||||||
Tags: netlify, edge-functions, ai, troubleshooting, help
|
Tags: netlify, edge-functions, ai, troubleshooting, help
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -877,6 +877,37 @@ Cards display post thumbnails (from `image` frontmatter field), titles, excerpts
|
|||||||
|
|
||||||
**View preference:** User's view mode choice is saved to localStorage and persists across page visits.
|
**View preference:** User's view mode choice is saved to localStorage and persists across page visits.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
### Homepage Post Limit
|
||||||
|
|
||||||
|
Limit the number of posts shown on the homepage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
postsDisplay: {
|
||||||
|
showOnHome: true,
|
||||||
|
homePostsLimit: 5, // Limit to 5 most recent posts (undefined = show all)
|
||||||
|
homePostsReadMore: {
|
||||||
|
enabled: true,
|
||||||
|
text: "Read more blog posts",
|
||||||
|
link: "/blog",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
When posts are limited, an optional "read more" link appears below the list. Only shows when there are more posts than the limit.
|
||||||
|
|
||||||
### Hardcoded Navigation Items
|
### Hardcoded Navigation Items
|
||||||
|
|
||||||
Add React route pages (like `/stats`, `/write`) to the navigation menu via `siteConfig.ts`. These pages are React components, not markdown files.
|
Add React route pages (like `/stats`, `/write`) to the navigation menu via `siteConfig.ts`. These pages are React components, not markdown files.
|
||||||
@@ -1044,6 +1075,14 @@ Pages appear automatically in the navigation when published.
|
|||||||
|
|
||||||
**Right sidebar:** When enabled in `siteConfig.rightSidebar.enabled`, posts and pages can display a right sidebar containing the CopyPageDropdown at 1135px+ viewport width. Add `rightSidebar: true` to frontmatter to enable. Without this field, pages render normally with CopyPageDropdown in the nav bar. When enabled, CopyPageDropdown moves from the navigation bar to the right sidebar on wide screens. The right sidebar is hidden below 1135px, and CopyPageDropdown returns to the nav bar automatically.
|
**Right sidebar:** When enabled in `siteConfig.rightSidebar.enabled`, posts and pages can display a right sidebar containing the CopyPageDropdown at 1135px+ viewport width. Add `rightSidebar: true` to frontmatter to enable. Without this field, pages render normally with CopyPageDropdown in the nav bar. When enabled, CopyPageDropdown moves from the navigation bar to the right sidebar on wide screens. The right sidebar is hidden below 1135px, and CopyPageDropdown returns to the nav bar automatically.
|
||||||
|
|
||||||
|
**Show image at top:** Add `showImageAtTop: true` to display the `image` field at the top of the post/page above the header. Default behavior: if `showImageAtTop` is not set or `false`, image only used for Open Graph previews and featured card thumbnails.
|
||||||
|
|
||||||
|
**Footer:** Footer content can be set in frontmatter (`footer` field) or use `siteConfig.footer.defaultContent`. Control visibility globally via `siteConfig.footer.enabled` and per-page via `showFooter: true/false` frontmatter.
|
||||||
|
|
||||||
|
**Social footer:** Display social icons and copyright below the main footer. Configure via `siteConfig.socialFooter`. Control visibility per-page via `showSocialFooter: true/false` frontmatter.
|
||||||
|
|
||||||
|
**Contact form:** Enable contact forms on any page or post via `contactForm: true` frontmatter. Requires `AGENTMAIL_API_KEY` and `AGENTMAIL_INBOX` environment variables in Convex. See the [Contact Form section](#contact-form-configuration) below.
|
||||||
|
|
||||||
**AI Agent chat:** The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Enable Agent on the Write page via `siteConfig.aiChat.enabledOnWritePage` or in the right sidebar on posts/pages using `aiChat: true` frontmatter (requires `rightSidebar: true`). Requires `ANTHROPIC_API_KEY` environment variable in Convex. See the [AI Agent chat section](#ai-agent-chat) below for setup instructions.
|
**AI Agent chat:** The site includes an AI writing assistant (Agent) powered by Anthropic Claude API. Enable Agent on the Write page via `siteConfig.aiChat.enabledOnWritePage` or in the right sidebar on posts/pages using `aiChat: true` frontmatter (requires `rightSidebar: true`). Requires `ANTHROPIC_API_KEY` environment variable in Convex. See the [AI Agent chat section](#ai-agent-chat) below for setup instructions.
|
||||||
|
|
||||||
### Update SEO Meta Tags
|
### Update SEO Meta Tags
|
||||||
@@ -1059,6 +1098,14 @@ Edit `index.html` to update:
|
|||||||
|
|
||||||
Edit `public/llms.txt` and `public/robots.txt` with your site information.
|
Edit `public/llms.txt` and `public/robots.txt` with your site information.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
## Search
|
## Search
|
||||||
|
|
||||||
Your blog includes full text search with Command+K keyboard shortcut.
|
Your blog includes full text search with Command+K keyboard shortcut.
|
||||||
@@ -1101,6 +1148,137 @@ How it works:
|
|||||||
- A cron job cleans up stale sessions every 5 minutes
|
- A cron job cleans up stale sessions every 5 minutes
|
||||||
- No personal data is stored (only anonymous UUIDs)
|
- No personal data is stored (only anonymous UUIDs)
|
||||||
|
|
||||||
|
## Footer Configuration
|
||||||
|
|
||||||
|
The footer component displays markdown content and can be configured globally or per-page.
|
||||||
|
|
||||||
|
**Global configuration:**
|
||||||
|
|
||||||
|
In `src/config/siteConfig.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
footer: {
|
||||||
|
enabled: true, // Global toggle for footer
|
||||||
|
showOnHomepage: true, // Show footer on homepage
|
||||||
|
showOnPosts: true, // Default: show footer on blog posts
|
||||||
|
showOnPages: true, // Default: show footer on static pages
|
||||||
|
showOnBlogPage: true, // Show footer on /blog page
|
||||||
|
defaultContent: "...", // Default markdown content
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontmatter override:**
|
||||||
|
|
||||||
|
Set `showFooter: false` in post/page frontmatter to hide footer on specific pages. Set `footer: "..."` to provide custom markdown content.
|
||||||
|
|
||||||
|
**Footer images:** Footer markdown supports images with size control via HTML attributes (`width`, `height`, `style`, `class`).
|
||||||
|
|
||||||
|
## Social Footer Configuration
|
||||||
|
|
||||||
|
Display social icons and copyright information below the main footer.
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
In `src/config/siteConfig.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
socialFooter: {
|
||||||
|
enabled: true,
|
||||||
|
showOnHomepage: true,
|
||||||
|
showOnPosts: true,
|
||||||
|
showOnPages: true,
|
||||||
|
showOnBlogPage: true,
|
||||||
|
socialLinks: [
|
||||||
|
{ platform: "github", url: "https://github.com/username" },
|
||||||
|
{ platform: "twitter", url: "https://x.com/handle" },
|
||||||
|
{ platform: "linkedin", url: "https://linkedin.com/in/profile" },
|
||||||
|
],
|
||||||
|
copyright: {
|
||||||
|
siteName: "Your Site Name",
|
||||||
|
showYear: true, // Auto-updates to current year
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported platforms:** github, twitter, linkedin, instagram, youtube, tiktok, discord, website
|
||||||
|
|
||||||
|
**Frontmatter override:**
|
||||||
|
|
||||||
|
Set `showSocialFooter: false` in post/page frontmatter to hide social footer on specific pages.
|
||||||
|
|
||||||
|
## Right Sidebar Configuration
|
||||||
|
|
||||||
|
Enable a right sidebar on posts and pages that displays CopyPageDropdown at wide viewport widths.
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
In `src/config/siteConfig.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
rightSidebar: {
|
||||||
|
enabled: true, // Set to false to disable globally
|
||||||
|
minWidth: 1135, // Minimum viewport width to show sidebar
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontmatter usage:**
|
||||||
|
|
||||||
|
Enable right sidebar on specific posts/pages:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: My Post
|
||||||
|
rightSidebar: true
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
- Right sidebar appears at 1135px+ viewport width
|
||||||
|
- Contains CopyPageDropdown with sharing options
|
||||||
|
- Three-column layout: left sidebar (TOC), main content, right sidebar
|
||||||
|
- Hidden below 1135px, CopyPageDropdown returns to nav
|
||||||
|
|
||||||
|
## Contact Form Configuration
|
||||||
|
|
||||||
|
Enable contact forms on any page or post via frontmatter. Messages are sent via AgentMail.
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
|
||||||
|
Set these in the Convex dashboard:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ------------------------- | --------------------------------------------------------------------------- |
|
||||||
|
| `AGENTMAIL_API_KEY` | Your AgentMail API key |
|
||||||
|
| `AGENTMAIL_INBOX` | Your inbox address for sending |
|
||||||
|
| `AGENTMAIL_CONTACT_EMAIL` | Optional: recipient for contact form messages (defaults to AGENTMAIL_INBOX) |
|
||||||
|
|
||||||
|
**Site Config:**
|
||||||
|
|
||||||
|
In `src/config/siteConfig.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
contactForm: {
|
||||||
|
enabled: true, // Global toggle for contact form feature
|
||||||
|
title: "Get in Touch",
|
||||||
|
description: "Send us a message and we'll get back to you.",
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
**Frontmatter Usage:**
|
||||||
|
|
||||||
|
Enable contact form on any page or post:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: Contact Us
|
||||||
|
slug: contact
|
||||||
|
contactForm: true
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
The form includes name, email, and message fields. Submissions are stored in Convex and sent via AgentMail to the configured recipient.
|
||||||
|
|
||||||
## Newsletter Admin
|
## Newsletter Admin
|
||||||
|
|
||||||
A newsletter management interface is available at `/newsletter-admin`. Use it to view subscribers, send newsletters, and compose custom emails.
|
A newsletter management interface is available at `/newsletter-admin`. Use it to view subscribers, send newsletters, and compose custom emails.
|
||||||
|
|||||||
@@ -171,6 +171,17 @@ export interface WeeklyDigestConfig {
|
|||||||
subject: string; // Email subject template
|
subject: string; // Email subject template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MCP Server configuration
|
||||||
|
// HTTP-based Model Context Protocol server for AI tool integration
|
||||||
|
// Runs on Netlify Edge Functions at /mcp endpoint
|
||||||
|
export interface MCPServerConfig {
|
||||||
|
enabled: boolean; // Global toggle for MCP server
|
||||||
|
endpoint: string; // Endpoint path (default: "/mcp")
|
||||||
|
publicRateLimit: number; // Requests per minute for public access
|
||||||
|
authenticatedRateLimit: number; // Requests per minute with API key
|
||||||
|
requireAuth: boolean; // Require API key for all requests
|
||||||
|
}
|
||||||
|
|
||||||
// Social link configuration for social footer
|
// Social link configuration for social footer
|
||||||
export interface SocialLink {
|
export interface SocialLink {
|
||||||
platform:
|
platform:
|
||||||
@@ -277,6 +288,9 @@ export interface SiteConfig {
|
|||||||
|
|
||||||
// Weekly digest configuration (optional)
|
// Weekly digest configuration (optional)
|
||||||
weeklyDigest?: WeeklyDigestConfig;
|
weeklyDigest?: WeeklyDigestConfig;
|
||||||
|
|
||||||
|
// MCP Server configuration (optional)
|
||||||
|
mcpServer?: MCPServerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default site configuration
|
// Default site configuration
|
||||||
@@ -556,6 +570,18 @@ Created by [Wayne](https://x.com/waynesutton) with Convex, Cursor, and Claude Op
|
|||||||
dayOfWeek: 0, // Sunday
|
dayOfWeek: 0, // Sunday
|
||||||
subject: "Weekly Digest", // Email subject prefix
|
subject: "Weekly Digest", // Email subject prefix
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// MCP Server configuration
|
||||||
|
// HTTP-based Model Context Protocol server for AI tool integration
|
||||||
|
// Runs on Netlify Edge Functions at /mcp endpoint
|
||||||
|
// Set MCP_API_KEY in Netlify env vars for authenticated access
|
||||||
|
mcpServer: {
|
||||||
|
enabled: true, // Global toggle for MCP server
|
||||||
|
endpoint: "/mcp", // Endpoint path
|
||||||
|
publicRateLimit: 50, // Requests per minute for public access (Netlify rate limiting)
|
||||||
|
authenticatedRateLimit: 1000, // Requests per minute with API key
|
||||||
|
requireAuth: false, // Set to true to require API key for all requests
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export the config as default for easy importing
|
// Export the config as default for easy importing
|
||||||
|
|||||||
Reference in New Issue
Block a user