A web application for annotating Markdown specifications with highlights and comments, designed to generate structured feedback for LLM coding agents.
- Markdown Input: Paste any Markdown document into a large textarea
- Text Selection: Select any text in the rendered Markdown to add feedback
- Floating Annotations: Compact floating UI for adding comments without leaving context
- Visual Highlights: Yellow highlights for existing annotations, blue for active selection
- Share Codes: Generate short 6-character codes for easy mobile sharing
- Export: Copy all feedback as formatted Markdown to clipboard
- Persistent Annotations: Annotations saved to localStorage, keyed by content or share code
Specmark uses Umami Analytics for anonymous, privacy-friendly usage tracking. The tracker script is injected at runtime when the following environment variables are set:
VITE_UMAMI_SCRIPT_URL(e.g.https://analytics.example.com/script.js)VITE_UMAMI_WEBSITE_ID(the Umami website UUID)- Optional:
VITE_UMAMI_HOST_URL(if your script is hosted separately from the collector) - Optional:
VITE_UMAMI_DOMAINS(comma-separated list of allowed domains)
The app records:
- Page views (default Umami behavior)
Share Createwhen a share code is successfully createdShare Loadwhen a share code is successfully retrievedCopy Allwhen feedback is copied to the clipboard
No PII or document content is sent with these events.
- React 19
- Vite
- Tailwind CSS + @tailwindcss/typography
- react-markdown
- Cloudflare Workers
- Hono (TypeScript)
- Cloudflare KV (storage)
specmark/
├── src/ # Frontend React app
├── worker/ # Cloudflare Worker API
│ ├── src/
│ │ ├── index.ts # Hono app entry point
│ │ ├── routes/share.ts # API routes
│ │ └── lib/codes.ts # Code generation
│ ├── wrangler.toml # Worker config
│ └── package.json
├── cli/
│ └── specmark # CLI tool for creating shares
├── specs/ # Feature specifications
└── .github/workflows/ # GitHub Actions
- Node.js 20+
- npm
# Install frontend dependencies
npm install
# Install worker dependencies
cd worker && npm install# Terminal 1: Start frontend
npm run dev
# Terminal 2: Start worker API
cd worker && npm run dev- Frontend: http://localhost:5173
- Worker API: http://localhost:8787
- Paste Markdown: Enter your Markdown content in the textarea
- Get Share Code: Click "Get Share Code" to generate a short code for sharing
- Start Annotating: Click "Start Annotating" to enter annotation mode
- Select Text: Highlight text to add feedback via the floating comment box
- Copy Feedback: Click "Copy All" to export annotations as Markdown
- Via URL:
https://specmark.dev?c=X7KM3P - Via Code Entry: Enter the 6-character code in the header input field
Create share links directly from your terminal:
# Add to PATH or use directly
./cli/specmark path/to/document.md
# Output:
# Share created!
# URL: https://specmark.dev?c=X7KM3P
# Code: X7KM3P
# Expires: 2025-01-10For local development:
export SPECMARK_API_URL=http://localhost:8787
./cli/specmark document.mdThe original base64 URL encoding is still supported:
?markdown=<base64-encoded-content>?md=<url-encoded-content>
Specmark uses Cloudflare Pages for static hosting and Cloudflare Workers for the API, with path-based routing (/api/*) to keep everything on the same domain.
- Frontend:
https://specmark.dev(Cloudflare Pages) - API:
https://specmark.dev/api/*(Cloudflare Worker) - Routing:
public/_routes.jsonexcludes/api/*from Pages, routes to Worker
This same-origin setup eliminates CORS complexity and provides a cleaner user experience.
# Login to Cloudflare
cd worker
npx wrangler login
# Create production KV namespace
npx wrangler kv:namespace create SHARES --env production
# Note the ID returned, e.g., "abc123..."Edit worker/wrangler.toml and add the KV namespace ID:
[env.production]
kv_namespaces = [
{ binding = "SHARES", id = "your-kv-namespace-id-here" }
]
vars = { FRONTEND_URL = "https://specmark.dev" }Deploy the Worker to specmark.dev/api/*:
cd worker
npx wrangler deploy --env production --route "specmark.dev/api/*"Via GitHub Actions (Automated):
- Create a Cloudflare API token at https://dash.cloudflare.com/profile/api-tokens
- Use the "Edit Cloudflare Workers" template or create custom token with:
- Workers Scripts: Edit
- Workers KV Storage: Edit
- Cloudflare Pages: Edit
- Use the "Edit Cloudflare Workers" template or create custom token with:
- Add GitHub secrets at
https://github.com/YOUR_USERNAME/specmark/settings/secrets/actions:CLOUDFLARE_API_TOKEN- Your Cloudflare API tokenVITE_UMAMI_SCRIPT_URL- (Optional) Your Umami analytics script URLVITE_UMAMI_WEBSITE_ID- (Optional) Your Umami website ID
- Push to main branch:
- Worker changes (
worker/**) trigger worker deployment - Frontend changes (
src/**,public/**) trigger Pages deployment
- Worker changes (
Option A: GitHub Integration (Recommended)
- Go to Cloudflare Pages
- Connect your GitHub repository
- Configure build settings:
- Build command:
npm run build - Build output directory:
dist - Root directory:
/(leave empty)
- Build command:
- Deploy
The public/_routes.json file automatically configures routing so /api/* requests go to your Worker.
Option B: Manual Deployment
npm run build
npx wrangler pages deploy dist --project-name=specmarkIn Cloudflare DNS, set up your domain:
- Add your domain to Cloudflare (if not already)
- Cloudflare Pages will automatically configure the DNS records
- Your Worker route (
specmark.dev/api/*) uses the same domain
Test that the setup works:
# Frontend (should return HTML)
curl https://specmark.dev
# API health check (should return JSON)
curl https://specmark.dev/health
# API endpoint (should work)
curl -X POST https://specmark.dev/api/share \
-H "Content-Type: text/markdown" \
-d "# Test"To test against production:
# Frontend
VITE_API_URL=https://specmark.dev npm run dev
# CLI
export SPECMARK_API_URL=https://specmark.dev
./cli/specmark document.mdPOST /api/share
Content-Type: text/markdown
<markdown content>
Response (201):
{
"code": "X7KM3P",
"url": "https://example.com?c=X7KM3P",
"expiresAt": "2025-01-10T00:00:00.000Z"
}GET /api/share/:code
Response (200):
{
"markdown": "# Document content...",
"createdAt": "2025-01-03T00:00:00.000Z",
"expiresAt": "2025-01-10T00:00:00.000Z"
}Exported feedback is formatted as Markdown:
## Feedback
> Selected text from the document
Your feedback comment here
---
> Another selected passage
Another comment- Cmd/Ctrl + Enter: Save feedback in comment dialog
- Escape: Cancel comment dialog
- 6 characters from
[2-9A-HJKMNP-Z](excludes confusable characters) - Case-insensitive
- ~887 million possible combinations
Shares expire after 7 days automatically via Cloudflare KV TTL.
- Maximum markdown size: 500KB