Deployment

Emet is a static site: npm run build gives you a dist/ directory of HTML, JavaScript and CSS that any web server can serve. There’s one catch, and it’s the reason this section exists. Signing in with GitHub needs a token exchange that the browser can’t do on its own, and that one endpoint has to run somewhere.

Why there’s a server-side piece at all#

GitHub’s OAuth flow ends with the app swapping an authorization code for an access token. That request has to carry the GitHub App’s client secret, and it has to go to github.com, which doesn’t send CORS headers. So a browser-only app can’t finish the login. Emet gets around this with a tiny proxy: the browser posts the code to /api/github/token on the same origin, and the proxy adds the secret, forwards the request to GitHub and hands the response back. Everything else, from reading your repository to merging pull requests, is the browser talking to GitHub’s API directly with the token it got.

The proxy lives in functions/api/github/token.ts, written as a Cloudflare Pages Function. There are three more functions under functions/api/gsc/ that do the same job for Google Search Console, and a small functions/_middleware.ts that decides which other origins may call them. That’s the entire server-side footprint of the project. Deploy to Cloudflare Pages and these come along for free; deploy anywhere else and you can either still borrow a Pages project for them or provide an equivalent.

The two routes#

  • Cloudflare Pages is the path of least resistance. npm run deploy builds and pushes with wrangler, or you connect the repository and let Pages build on every push. Either way the functions are picked up automatically.
  • Self-hosting covers everything else: Netlify, Vercel, GitHub Pages, a directory on your own server or dropping the build into your Hugo site’s static/ folder. The static files are easy; the page is mostly about the proxy you’ll need to run alongside them and the VITE_API_BASE_URL setting that points Emet at it.

What every deployment needs#

Whichever route you take, you’ll need:

  • Node.js 22.12 or later to build.
  • A GitHub App with its callback URL set to the address you’ll serve Emet from. Its client ID is baked into the build with VITE_GITHUB_CLIENT_ID; its client secret goes to the proxy as GITHUB_CLIENT_SECRET and never reaches the browser.
  • Somewhere to run the token exchange.

Note that a build without VITE_GITHUB_CLIENT_ID isn’t broken, exactly, but it isn’t useful either: Emet starts up and shows “Missing build-time configuration” instead of a sign-in button. The full list of variables is in Environment variables.

One deployment, many sites#

The repository, branch and (if your Hugo site lives in a subdirectory) base path are all chosen after sign-in, so a single deployment of Emet can edit any repository the GitHub App is installed on. Site-specific configuration lives in the repository itself as admin/site.yaml and admin/cms.yaml, which also means each branch can carry its own.

If you want separate staging and production copies of the CMS itself, deploy twice to two hostnames and add both as callback URLs on your GitHub App. They can share one set of functions: list both origins in ALLOWED_ORIGINS and point one copy’s VITE_API_BASE_URL at the other.

Updating#

With Git integration on Cloudflare Pages (or any host that builds on push), updating is git pull from upstream and pushing to your fork. For a manual deployment it’s the same three steps as the first time:

git pull && npm install && npm run build

then upload dist/ again, or npm run deploy on Cloudflare.