A personal portfolio built with Astro and Draftbase. Content is fetched at build time and deployed to GitHub Pages as static HTML — no server, no runtime API calls, nothing to pay for.
This is the simplest example. It uses one content query, media fields, and rich text. For references and pagination see example-blog.
npm install
cp .env.example .env # add your API keys
npm run seed # creates the templates + sample content in your org
npm run devnpm run seed is optional — if you already have profile and project templates, skip it.
Seeded by scripts/seed.mjs. A template's key (profile, project) is what the
delivery API calls templateId.
profile — a single entry describing you.
| Field | Type | |
|---|---|---|
name |
text | required |
headline |
text | required |
bio |
richText | rendered as markdown/MDX |
avatar |
media | |
email, githubUrl, linkedinUrl |
text |
project
| Field | Type | |
|---|---|---|
title |
text | required |
slug |
text | required, used as the URL |
summary |
text | max 200 chars |
body |
richText | |
cover |
media | |
liveUrl |
text | |
year |
number | sorts the list, newest first |
featured |
boolean |
src/lib/draftbase.tscreates the SDK client and exposesgetAll(templateId), which follows cursor pagination to the end.- Pages call it in their frontmatter, so every request happens during
astro build. include: 1resolvesmediafields into objects with aurl, instead of raw asset ids.- Rich text goes through
toHtml()from@draftbase/rendererand is injected withset:html. Style it via the.db-contentclass.
reactis independencieseven though this site ships no React.@draftbase/rendererexportstoHtmlandMDXContentfrom one entry point, so importing either one pulls the React import in at bundle time. No React reaches the browser — Astro renders this at build time and the output is plain HTML.
The delivery API key is build-time only:
- It is read through
import.meta.env.DRAFTBASE_API_KEYin server code. Astro only exposesPUBLIC_-prefixed variables to the browser, so this one cannot end up in the bundle. Do not rename it toPUBLIC_DRAFTBASE_API_KEY. - In CI it comes from the
DRAFTBASE_API_KEYrepository secret. - Use a delivery-scoped key. It is read-only and only ever returns published entries.
- The management key (
DRAFTBASE_MANAGEMENT_API_KEY) is only fornpm run seed, and belongs in your local.envand nowhere else. It can write and delete content. .envis gitignored. Only.env.example, which holds no values, is committed.
Verify for yourself after a build: grep -r "$(grep DRAFTBASE_API_KEY .env | cut -d= -f2)" dist/
should find nothing.
- Settings → Pages → Source: GitHub Actions.
- Settings → Secrets and variables → Actions → add
DRAFTBASE_API_KEY(delivery-scoped). - Optional: on the same page, add a repository variable
DRAFTBASE_ENVIRONMENTif your content lives in an environment other thanproduction. - Push to
main.
This repo deploys to the custom domain in public/CNAME, so it sets no base.
Forking to a project site (<user>.github.io/<repo>)? Delete public/CNAME and add
base: "/<repo>" to astro.config.mjs — GitHub Pages serves project sites
from a subpath.
Draftbase can call GitHub for you. Create a webhook pointing at:
POST https://api.github.com/repos/<owner>/example-portfolio/dispatches
{ "event_type": "draftbase-publish" }
with an Authorization: Bearer <fine-grained PAT> header, scoped to this repo with
Contents: read and write. That token lives in Draftbase's webhook config — never in this
repo.
- Contact form — needs a server. Use a
mailto:link or an external form service. - Draft previews — need a running server; static builds only ever see published entries.