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.
- 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
focusconsultingorganization before creating a session. - Session is an encrypted,
HttpOnlycookie. The GitHub token never reaches browser JavaScript; the app calls GitHub through/api/github/*, which is a proxy restricted tofocus-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.mdxusing 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 toblog/<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.
Requirements: Node 22.12+, pnpm 10.
pnpm install
cp .dev.vars.example .dev.vars # fill in the values below
pnpm devpnpm 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 |
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.
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.
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.
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)
- Slugs are generated from the title and locked once a branch exists.
- The
authorfield defaults to the GitHub profile name and is remembered after the first submission. - The
publishedtoggle maps to thepublishedfrontmatter flag; the site only renders posts where it istrue. - Curly braces in body text are escaped for MDX; angle brackets are written as entities. Inline code and code blocks are left untouched.