Skip to content

Repository files navigation

Focus Blog Writer

A dark-mode, mobile-friendly writing app for the Focus blog. Authors log in with GitHub, write in a WYSIWYG editor, and submit posts as pull requests to focusconsulting/focus-website. There is no database: GitHub is the backend and drafts live in the browser.

How it works

  • Login uses a real GitHub OAuth web flow handled by Cloudflare Pages Functions. The callback checks that the account is an active member of the focusconsulting organization before creating a session.
  • Session is an encrypted, HttpOnly cookie. The GitHub token never reaches browser JavaScript; the app calls GitHub through /api/github/*, which is a proxy restricted to focus-website, the org, and the signed-in user.
  • Editor is TipTap (ProseMirror) with headings, lists, quotes, links, inline code, highlighted code blocks, and images. Paste or drop an image anywhere and it is stored in IndexedDB next to the draft.
  • Drafts autosave to IndexedDB in the browser. The Posts page warns that drafts are local only.
  • Submit serializes the post to src/blog/<slug>/index.mdx using the site's frontmatter schema (title, description, publishDate, author, published). Images are committed into the same folder and referenced with Astro's <Image /> component, matching the site README. The Git Data API writes a commit to blog/<slug> and opens a pull request, or pushes a revision to the existing one.
  • Edit existing posts from the Posts page. The MDX is parsed back into the editor, images are pulled down into IndexedDB, and submitting opens an update PR.

Local development

Requirements: Node 22.12+, pnpm 10.

pnpm install
cp .dev.vars.example .dev.vars   # fill in the values below
pnpm dev

pnpm dev runs Vite on http://localhost:5173 and wrangler pages dev on port 8788 for the functions. Vite proxies /api to wrangler. Keep APP_ORIGIN=http://localhost:5173 in .dev.vars so the OAuth redirect comes back to Vite.

Other commands:

Command Action
pnpm typecheck Type-check the app and the functions
pnpm build Type-check and build into dist/
pnpm preview Build and serve dist/ with functions through wrangler
pnpm deploy Build and deploy with wrangler pages deploy

GitHub OAuth App

Create one OAuth App per environment (GitHub allows a single callback URL per app). In the focusconsulting org: Settings → Developer settings → OAuth Apps → New OAuth App.

  • Homepage URL: the app URL, for example https://focus-blog-writer.pages.dev
  • Authorization callback URL: https://<app-domain>/api/auth/callback (for local dev, http://localhost:5173/api/auth/callback)

The app requests the repo and read:org scopes. If the organization has third-party application access restrictions turned on, an org owner must approve the OAuth App, otherwise the token cannot see org repositories or membership.

Cloudflare Pages

Create a Pages project from this repository with:

  • Build command: pnpm build
  • Build output directory: dist
  • Functions are picked up automatically from functions/

Set these in Settings → Environment variables (mark the secrets as encrypted):

Name Purpose
GITHUB_CLIENT_ID OAuth App client ID
GITHUB_CLIENT_SECRET OAuth App client secret
SESSION_SECRET Long random string used to encrypt the session cookie
GITHUB_ORG Optional, defaults to focusconsulting
GITHUB_REPO Optional, defaults to focus-website
APP_ORIGIN Optional, only needed when the app is served behind a different hostname

Generate a session secret with openssl rand -base64 48.

Testing before the OAuth App is approved

While an org owner has not yet approved the OAuth App, tokens cannot read org membership or org repositories. Set SKIP_ORG_CHECK=true in .dev.vars (or as a Pages environment variable) to skip the membership gate. Remove it from production as soon as the app is approved, since it lets any GitHub account log in. To exercise submissions end to end, fork focus-website into your personal account and set GITHUB_ORG to your username; the app targets whatever GITHUB_ORG/GITHUB_REPO the server reports.

Project layout

functions/
  _lib/                 session encryption, cookies, GitHub helpers
  api/auth/             login, callback, logout
  api/me.ts             current user
  api/github/[[path]].ts authenticated GitHub proxy
src/
  components/           shell, editor, preview, modals
  lib/                  config, GitHub client, IndexedDB, MDX conversion, submit flow
  pages/                login, posts, new post, editor
  styles/global.css     brand tokens and utility classes (dark only)

Content conventions

  • Slugs are generated from the title and locked once a branch exists.
  • The author field defaults to the GitHub profile name and is remembered after the first submission.
  • The published toggle maps to the published frontmatter flag; the site only renders posts where it is true.
  • Curly braces in body text are escaped for MDX; angle brackets are written as entities. Inline code and code blocks are left untouched.

Releases

Packages

Contributors

Languages