From connecting a GitHub repo and deploying, to building pages, components, conditional logic, and article templates — everything you need to run your site.
Stencil stores everything as files in a GitHub repository — there's no database. You need a content repo, an OAuth App so people can sign in, and (optionally) a token.
Create a GitHub repository for your content and make at least one commit (e.g. add a README) — an empty repo has no default branch. Note the owner and name for GITHUB_OWNER and GITHUB_REPO.
Create an OAuth App for sign-in: GitHub → Settings → Developer settings → OAuth Apps → New OAuth App. Set the Authorization callback URL to <your-site-origin>/auth/github/callback. Copy the Client ID and generate a client secret.
GITHUB_OAUTH_CLIENT_ID=<client id>
GITHUB_OAUTH_CLIENT_SECRET=<client secret>Roles are derived from each signer's permission on the repo: admin → Admin, maintain → Moderator, write → Editor. Anything below write can't sign in.
Optional: create a fine-grained token (Developer settings → Personal access tokens → Fine-grained) scoped to the content repo with Contents = Read and write and Metadata = Read. Required if the repo is private and you want anonymous visitors to see the published site; otherwise commits are attributed to whoever is signed in.
GITHUB_TOKEN=github_pat_... # optionalCopy .env.example to .env, fill in the values above, and set a random SESSION_SECRET. Then install and run.
cp .env.example .env
npm install
npm run devDeploy the Stencil app (this project) to Vercel. Your content lives in the separate GitHub content repo you set up above.
Push this project to its own GitHub repository (separate from your content repo).
In Vercel: New Project → import that repo. The React Router build is detected automatically, and Node 22 is used (pinned in package.json).
Add the same environment variables from your .env under Project → Settings → Environment Variables: GITHUB_OWNER, GITHUB_REPO, GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET, SESSION_SECRET, plus optional GITHUB_TOKEN and API_TOKEN.
Point your OAuth App's callback URL at production: https://<your-domain>/auth/github/callback. An OAuth App allows one callback URL — use a separate app (or a GitHub App) for dev vs prod.
Deploy. Public pages are edge-cached (s-maxage), and the /content admin routes are already no-store.
Pages are built with the drag-and-drop visual builder and output clean, self-hosted HTML.
Go to /content → New Post → choose Page (Visual Builder). Give it a title.
Drag blocks from the palette (Layout, Basic, Components, Articles) onto the canvas. Select any element to edit its Tailwind classes, inline styles, and attributes in the Properties panel; reorder in the Layers tree.
Site-wide background and typography come from /content/settings (body classes) and apply to every page.
Save (commits to the draft branch), then Publish when it's ready.
Any page can be served at a custom public path once published — including the site root.
In the page editor, open Page settings and set the URL Path field, e.g. /about. It serves publicly at that path once published.
To make a page your home page, set the URL Path to /. The published page assigned to / overrides the default landing page.
Publish, then open the path in a signed-out browser to confirm it's public. Reserved prefixes (/content, /api, /articles, /embed, /login, …) can't be used as a page path.
Build a fragment once — a nav, a footer, a call-to-action — and drop it into any page. Edits propagate to every page that uses it.
Go to /components → + New. Enter a Name and Slug, pick a Category, and leave Type as Static. Create.
You land in the component editor — build it with the same drag-and-drop blocks.
In any Page, open the blocks palette → Components category → drag your component in. Update the component later and every page picks up the change.
A conditional component renders a different branch per visitor, evaluated on the server at request time.
Go to /components → + New and set Type to “Conditional — show a branch based on rules”. Create.
In the flow editor, add branches. Each condition is built from signals: auth (logged in, username, roles), query.<param>, data.<key> (page frontmatter), device, time.*, geo.*, and a stable ab.* bucket. Assign a target component to each branch, plus a default fallback.
Use the test-signals preview to see which branch wins, then Save.
Drop the conditional component into a page from the Components palette. Pages that use conditionals are served uncached (private, no-store) so per-visitor markup is never shared.
Articles (served at /articles/<slug>) render inside a Page you designate as their layout, so every article shares your header, footer, and styling.
Create a Page for the layout (as in step 03) — add your nav, footer, sidebars, and any surrounding chrome.
From the palette's Articles category, drag in the Article Content block where the article's header image + body should appear. Publish the page.
Go to /content/settings → Article Template → select that page. Every article now renders inside it. Pick “Default layout (no template)” to revert to the plain centered layout.