3 min read

I built a writing editor for my personal site this weekend

Writing Editor

Draft, review and publish your own personal writing with the help of AI reviews.

Software Engineering

Sajiv Francis editorial card
Sajiv Francis editorial card

I’ve been rebuilding my personal site for a couple of weeks. This post is about one specific feature: a writing editor that lets me draft a post, get it reviewed by Claude against my actual code and docs, and publish directly to sajivfrancis.com — all without leaving the browser. Drafts live in Cloudflare KV — DRAFTS_KV, owner-only behind CHAT_TOKEN. Once published, only the final version goes to GitHub; the site builds from there.

Here’s the loop. One Cloudflare Worker — the chat-worker — handles all read/write to DRAFTS_KV, owner-only access enforced by CHAT_TOKEN.

flowchart TD
    A[/admin/write] -->|Save| B[DRAFTS_KV<br>owner-only via CHAT_TOKEN]
    B -->|Publish| C[GitHub: src/content/blog/<br>YYYY-MM-DD-slug.mdx]
    C -->|git push| D[GitHub Pages rebuild]
    D --> E[sajivfrancis.com/blog/slug]
mindmap
  root((Personal Site Writing Editor))
    Draft Management
      Cloudflare KV Store
        DRAFTS_KV binding
        Owner-only access
        CHAT_TOKEN enforcement
      Browser Editor
        /admin/write UI
        Save draft
        Review trigger
        Publish action
    chat-worker
      Handles all KV read/write
      Single Cloudflare Worker
      Routes POST /draft
      Routes POST /review
      Routes POST /publish
    Review Pass
      Fetch GitHub repo source
      Ground prose against code
      Flag technical inaccuracies
      Manual fix before publish
    Corpus Grounding
      pgvector store
        topK 6 chunks
        Topic filters docs writing meta
        Query from title plus first 800 chars
      docs.sajivfrancis.com
      Cross-check against prior writing
    Claude AI Review
      Prose quality suggestions
      Technical claim validation
      Called by chat-worker
      Only as good as its grounding
    Publish Pipeline
      Push MDX to GitHub
        src/content/blog/YYYY-MM-DD-slug.mdx
      GitHub Pages rebuild
      Live at sajivfrancis.com/blog/slug
    Access Control
      CHAT_TOKEN on all endpoints
      Drafts stay private
      Only final version goes to GitHub

The review pass fetches the actual GitHub source for any repo I tag, then asks Claude to ground my prose against that code. If I write “I used Python” but ‘wrangler.toml’ says TypeScript, the suggestion flags it — I fix it manually before publishing.

This is a different way of writing. Review prose and technical claims in the same pass — fix what’s flagged, ground the build details against the actual repo, publish. The corpus grounding goes further: retrieval pulls from docs.sajivfrancis.com and the pgvector store, so I can cross-check a new post against everything I’ve already written and documented.

ComponentRoleAccess
/admin/writeBrowser editor UIOwner-only (CHAT_TOKEN)
chat-workerDraft read/write to KVOwner-only (CHAT_TOKEN)
DRAFTS_KVCloudflare KV draft storePrivate binding
GitHub repoPublished MDX + imagesPublic on push
pgvector storeCorpus chunk retrieval (topK: 6)Owner-only (CHAT_TOKEN)
ClaudeProse + technical reviewCalled by chat-worker
sequenceDiagram
    actor Owner
    participant Editor as /admin/write
    participant Worker as chat-worker
    participant KV as DRAFTS_KV
    participant GH as GitHub
    participant Claude
    participant PG as pgvector store

    Owner->>Editor: Save draft
    Editor->>Worker: POST /draft (CHAT_TOKEN)
    Worker->>KV: Write draft
    Owner->>Editor: Review
    Editor->>Worker: POST /review (CHAT_TOKEN)
    Worker->>GH: Fetch repo source
    Worker->>PG: Retrieve corpus chunks (topK=6)
    Worker->>Claude: Prose + grounding payload
    Claude-->>Worker: Suggestions
    Worker-->>Editor: Display suggestions
    Owner->>Editor: Publish
    Editor->>Worker: POST /publish (CHAT_TOKEN)
    Worker->>GH: Push MDX to src/content/blog/

The retrieval query is constructed from the draft’s title and the first 800 characters of the body — enough signal to surface relevant chunks without overfitting to the opening paragraph. The actual call looks like this:

// pgvector corpus grounding — use draft title + body lead as the
// retrieval query. Owner mode so all visibility scopes are in play.
let corpusChunks = [];
const retrievalQuery = `${draft.frontmatter.title}\n\n${draft.body.slice(0, 800)}`;
try {
  const ctx = await getContext(
    retrievalQuery,
    ['docs', 'writing', 'meta'],   // ← topic filters
    undefined,                       // owner → no visibility filter
    'owner',
    clientId,
    env,
    { topK: 6 }                      // ← matches topK in component table
  );
  corpusChunks = ctx.chunks ?? [];
} catch (e) {
  // Non-fatal — continue with GitHub grounding only
}

The AI layer is only as useful as its grounding — GitHub source, personal docs, the vector store. Strip that away and it is just a grammar checker.

Comments