- Astro 46.1%
- TypeScript 32.6%
- CSS 10%
- Shell 5.8%
- JavaScript 5.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .woodpecker | ||
| ci | ||
| public | ||
| src | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| AGENTS.md | ||
| astro.config.mjs | ||
| CLAUDE.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| skills-lock.json | ||
| tsconfig.json | ||
webhofer.dev
Personal site of Matthias Webhofer. Astro, static output, no client-side framework.
Currently a single landing page: name, role, one line of context, and a link to git.webmatix.dev.
Development
npm install
npm run dev
Serves on http://localhost:4321.
| Command | Action |
|---|---|
npm run dev |
Local dev server |
npm run build |
Type-check, then build to ./dist/ |
npm run preview |
Serve the production build locally |
npm run check |
astro check only |
npm run format |
Prettier |
Structure
.woodpecker/
check.yaml prettier gate, every branch
deploy.yaml build + deploy, one bucket per branch
ci/
deploy.sh rclone upload + prune, takes the bucket as its argument
public/ favicons, og-image.png
src/
components/ BaseHead, Button, Footer, ThemeToggle
layouts/
BaseLayout.astro <head>, SEO, fonts, theme script
pages/
index.astro the landing page
404.astro
robots.txt.ts robots.txt, disallow-all off production
styles/
global.css design tokens + Tailwind import
site.config.ts name, role, context line, git URL, production host
astro.config.mjs site URL, sitemap, fonts
Copy lives in src/site.config.ts. Colours are the six custom properties at the top of
src/styles/global.css, in a light and a dark block. Everything reads from those tokens,
nothing hardcodes a hex value.
Branches (jj bookmarks)
main: live state, deploys towebhofer.devdev: work in progress, deploys todev.webhofer.devtemplate: unmodified base from BracoZS/astro-starter-portfolio, kept for reference
template still holds the full template: work content collection, about page, header nav,
project pages. Those were removed on main and can be picked back from template when
needed.
Deployment
Woodpecker CI, two workflows in .woodpecker/:
| Workflow | Runs on | Does |
|---|---|---|
check.yaml |
every branch, every push/PR | npm run format:check |
deploy.yaml |
push to main or dev |
npm run build, then rclone the dist/ |
Targets are Garage buckets:
| Branch | Bucket | Site |
|---|---|---|
main |
portfolio-website |
webhofer.dev |
dev |
portfolio-website-dev |
dev.webhofer.dev |
No other branch deploys.
Setup
Two repository secrets in Woodpecker: garage_access_key and garage_secret_key. The
endpoint and region are literal in .woodpecker/deploy.yaml and marked # EDIT. The
region must match Garage's s3_region.
Per bucket, in Garage:
garage bucket create portfolio-website
garage bucket allow --read --write --key <key-id> portfolio-website
garage bucket website --allow portfolio-website
Read and write for the CI key, plus the bucket-level website grant for anonymous read. Garage does not implement object ACLs, so nothing is set per object.
How the upload works
ci/deploy.sh <bucket>, called once per branch from deploy.yaml. It
is plain rclone against a remote named garage, so it also runs by hand against a local
rclone.conf:
npm run build && ./ci/deploy.sh portfolio-website-dev
rclone sync deletes by default, so files dropped from dist/ do not accumulate in the
bucket. dist/ is therefore the complete truth for the bucket: anything put there by
hand is removed on the next deploy.
It runs in two passes because one rclone invocation can only set one Cache-Control.
Pass one copies the content-hashed _astro/ assets with max-age=31536000, immutable,
pass two syncs everything else with a short TTL and does the deleting. --checksum makes
pass two skip the already-uploaded assets, so they keep their long header. A
test -f dist/index.html guard stops a broken build from emptying the bucket.
The build needs egress to the npm registry, Docker Hub, and fonts.googleapis.com /
fonts.gstatic.com (Astro's Fonts API self-hosts the fonts at build time).
Environments
SITE_URL overrides the site origin, which feeds the canonical, Open Graph and sitemap
URLs. It defaults to https://webhofer.dev, so a plain npm run build is a production
build. Anything other than the production host also gets noindex, nofollow and a
disallow-all robots.txt, so the preview never competes with the live site.
SITE_URL=https://dev.webhofer.dev npm run build
The production host is PRODUCTION_HOST in src/site.config.ts, mirrored as
PRODUCTION_URL in astro.config.mjs. Both need changing together.
Known gaps
public/og-image.pngand the favicons are still template artwork.pnpm-lock.yamlandpnpm-workspace.yamlare stale template leftovers. The pipeline usesnpm ci, so they are inert, but they should go.
License
MIT, see LICENSE. Template by BracoZS, same licence.