- CSS 44.5%
- HTML 31.8%
- PHP 23.7%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| content | ||
| static | ||
| themes/expeditioner | ||
| .gitignore | ||
| .hugo_build.lock | ||
| hugo.toml | ||
| README.md | ||
expeditioner.nl
Personal website and blog for expeditioner.nu. Built with Hugo, hosted on boromir.ams1, deployed automatically via Forgejo Actions.
Table of contents
- How it works
- Repository structure
- Writing content
- Adding photos
- Theme and design
- Contact form
- Local development
- Deployment
- Server setup
- Troubleshooting
How it works
You edit or create a file in Forgejo (browser or git push)
→ Forgejo Actions triggers on push to main
→ Runner on boromir pulls the latest repo
→ Hugo builds the site into public/
→ Nginx serves public/ behind Nginx Proxy Manager
→ expeditioner.nu is live
Hugo is a static site generator. It takes Markdown files from content/ and HTML templates from themes/expeditioner/layouts/, combines them, and outputs a folder of plain HTML, CSS, and images into public/. There is no database, no PHP, no runtime — just files served directly by nginx.
Repository structure
expeditioner.nl/
│
├── hugo.toml # Site-wide configuration
│
├── content/ # All your pages and posts (Markdown)
│ ├── _index.md # Homepage content and front matter
│ ├── about.md # About page
│ ├── contact.md # Contact page
│ ├── work/
│ │ ├── _index.md # Work section index
│ │ ├── telecoms.md
│ │ ├── lighting.md
│ │ ├── festivals.md
│ │ └── colocation.md
│ └── blog/
│ ├── _index.md # Blog section index
│ └── norway-february.md # Example post
│
├── static/ # Files copied as-is into public/
│ ├── fonts/
│ │ └── CHARCUTERIE-BLOCK-W05-REGULAR.TTF
│ └── img/
│ ├── logo.png
│ └── *.jpg # All photo assets
│
├── themes/
│ └── expeditioner/ # Custom theme
│ ├── layouts/
│ │ ├── index.html # Homepage template
│ │ ├── _default/
│ │ │ ├── baseof.html # Base HTML wrapper (nav + footer)
│ │ │ ├── single.html # Single page template (about, contact, work pages)
│ │ │ └── list.html # List template (blog index, work index)
│ │ └── partials/
│ │ ├── head.html # <head> tag, CSS, meta tags
│ │ ├── nav.html # Navigation bar
│ │ └── footer.html # Footer
│ └── static/
│ └── css/
│ └── main.css # All site styles
│
├── nginx.conf # Nginx vhost config (reference copy)
│
├── .forgejo/
│ └── workflows/
│ └── deploy.yml # Forgejo Actions deploy workflow
│
└── .gitignore # Excludes public/ and resources/
Writing content
All content lives in content/ as Markdown files. Each file has a front matter block at the top (between the --- lines) containing metadata, followed by the page body in Markdown.
You can edit files directly in the Forgejo web interface — click the pencil icon on any file, make your changes, and commit. The site rebuilds automatically within a minute.
Blog posts
Create a new file in content/blog/ named your-post-title.md. The filename becomes the URL slug — norway-february.md becomes expeditioner.nu/blog/norway-february/.
Template:
---
title: "Your post title"
date: 2025-06-01
summary: "One or two sentences shown on the blog index and homepage."
tags: ["Overlanding", "Trip report"]
photo: "/img/your-photo.jpg"
---
Your post content goes here. Standard Markdown applies.
## A heading
A paragraph of text. **Bold** and *italic* work as expected.
Another paragraph.
Front matter fields:
| Field | Required | Description |
|---|---|---|
title |
yes | Post title |
date |
yes | Publication date (YYYY-MM-DD). Posts are ordered by this. |
summary |
yes | Short excerpt shown on index pages |
tags |
no | Array of tags shown under the post |
photo |
no | Path to a header image under /img/ |
Posts are sorted newest-first automatically. The homepage shows the three most recent.
Work pages
Work pages live in content/work/. Each one represents a discipline and appears as a card in the work grid on the homepage and work index.
Front matter fields:
| Field | Required | Description |
|---|---|---|
title |
yes | Job title shown on the card |
summary |
yes | Short description shown on the card |
label |
yes | Small tag line shown at the bottom of the card (e.g. Telecoms · Main gig) |
accent |
no | Hex colour for the card's accent bar and label. Defaults to green. |
photo |
no | Path to a card background image under /img/ |
weight |
no | Controls sort order. Lower numbers appear first. |
Editing static pages
about.md, contact.md, and content/work/_index.md are straightforward — edit the body text below the --- block. The front matter fields available are title, description, subtitle, and photo.
The homepage (content/_index.md) is slightly different — the body text appears in the About section. The hero title and subtitle are set in hugo.toml under [params].
Adding photos
All images go in static/img/. After adding a file there, reference it in front matter as /img/filename.jpg — Hugo handles the rest.
Expected image slots:
| File | Used by |
|---|---|
logo.png |
Nav bar, hero section, browser tab |
portrait.jpg |
Homepage about section |
about-hero.jpg |
About page header |
strip-road.jpg |
Photo strip slot 1 (homepage) |
strip-tower.jpg |
Photo strip slot 2 (homepage) |
strip-stage.jpg |
Photo strip slot 3 (homepage) |
work-telecoms.jpg |
Telecoms work card background |
work-lighting.jpg |
Lighting work card background |
work-festivals.jpg |
Festivals work card background |
work-colo.jpg |
Colocation work card background |
blog-*.jpg |
Individual blog post headers |
Images are not processed or resized by Hugo in the current setup — serve them at a reasonable resolution (1200–1800px wide for full-bleed shots, 800px for card backgrounds). Use JPEG for photos, PNG for the logo.
Strip the metadata before committing a photo. Phone photos carry EXIF, often including GPS coordinates, and everything in static/img/ is public. On 2026-10-16 hero-bg.jpg, portrait.jpg and strip-roadtrip.jpg all still had the location they were taken at; they were re-saved without EXIF (colour profile kept) and scaled down at the same time: hero-bg.jpg is 2560x1920 and 915 KB (was 7296x5472, 8.6 MB), the other two 1600 px on the long side. The page background is blurred and darkened, so it gains nothing from more pixels.
Deleting a file from static/ does not delete it from the live site. The deploy runs hugo --minify without --cleanDestinationDir, so the old copy stays in public/ on boromir until it is removed there by hand (as hero-bg.png was, 2026-10-16).
After adding images, commit them to the repo and the runner will rebuild automatically. Or rebuild manually:
cd /var/www/domains/expeditioner.nl && hugo --minify
Theme and design
The theme lives entirely in themes/expeditioner/. You should not need to touch it for day-to-day content editing.
Fonts:
- Display headings: Charcuterie Blocks — loaded from
/fonts/CHARCUTERIE-BLOCK-W05-REGULAR.TTF - Body text: Barlow Light — loaded from Google Fonts
Colours (CSS variables in main.css):
| Variable | Value | Used for |
|---|---|---|
--navy |
#070e1c |
Page background |
--green |
#4fc98a |
Primary accent, CTAs |
--teal |
#38bfbf |
Secondary accent, nav, eyebrows |
--violet |
#8b7cf8 |
Tertiary accent |
--amber |
#c8a045 |
Quaternary accent |
--frost |
#b8d4e0 |
Primary body text |
--muted |
#4a6a80 |
Secondary/dimmed text |
To change a colour site-wide, update the variable in :root {} at the top of main.css and rebuild.
The photo behind the page is a position: fixed body::before layer, with the content in a column that uses backdrop-filter: blur(12px). Never put it back as background-attachment: fixed on body: the browser then repaints the photo on every scroll frame and the blur is recomputed over it, which made scrolling feel heavy (fixed 2026-10-16). expeditioner.network carries the same rules in its copy of the theme.
Layout templates — if you want to change how a page type looks, edit the relevant file in themes/expeditioner/layouts/:
- Homepage layout:
index.html - Blog/work index pages:
_default/list.html - Individual pages and posts:
_default/single.html - Nav and footer:
partials/nav.html,partials/footer.html
Contact form
The form on the homepage posts to static/mail.php, which mails web@expeditioner.nl. Hardened
on 2026-08-29 after spam volume climbed; a honeypot and a 3 second timer were all it had.
What it does now, in order: honeypot, form-age window (3s to 2h), per-IP rate limit (3/hour, 8/day, counted before sending and recorded only on success, so a blocked spam run cannot lock out a real visitor on the same address), CR/LF rejection on name and email, field validation, then scoring.
Scoring refuses at 4 points. Anchor/BBCode markup 4, a URL in the name field 4, a foreign
Origin 4, four or more links 3, two or more links 2, Cyrillic/CJK 2, an SEO/crypto/casino
phrase 2, missing Origin and Referer 2. Nothing decides on its own except the three that
have no innocent reading. The score is included in the mail you receive, so you can see how close
real messages come to the line.
Scoring reads the raw POST values, not the sanitised ones: strip_tags() would remove the
<a href> that is the single most reliable signal before it could ever be counted.
Two things that are deliberate:
- No confirmation mail to the sender. There used to be one. It mailed any address a stranger
typed into the form, quoting that stranger's text, from an
expeditioner.nlsender - a free relay pointed at third parties and an easy way to burn the domain's reputation. - A newline in
nameis fatal. That field goes into aReply-Toheader, andh()strips tags but not newlines, so a CR/LF there ended the header and started another. That was live.
Every verdict, accepted or refused, is logged to <sys_get_temp_dir()>/expeditioner-contact/contact.log
as time, IP, verdict, detail, email. Read that before tuning anything: a filter whose output
nobody looks at cannot be tuned, and the first question after "is spam still arriving?" is always
"what did it stop?".
Local development
If you want to preview changes before pushing, install Hugo on your local machine and run the dev server:
# Clone the repo
git clone ssh://git@forge.exped.nu:222/Expeditioner/expeditioner.nl.git
cd expeditioner.nl
# Start the dev server — live-reloads on file changes
hugo server
# Open in browser
http://localhost:1313
Hugo version used in production: 0.128.0 extended. Install the same version locally to avoid any build differences.
curl -sSL https://github.com/gohugoio/hugo/releases/download/v0.128.0/hugo_extended_0.128.0_linux-amd64.tar.gz | tar -xz
sudo mv hugo /usr/local/bin/hugo
Deployment
Deployment is automatic. Every push to the main branch triggers the Forgejo Actions workflow at .forgejo/workflows/deploy.yml.
What the workflow does:
- Forgejo runner on boromir picks up the job
- Pulls latest from
maininto/var/www/domains/expeditioner.nl - Runs
hugo --minifyto rebuildpublic/ - Nginx immediately serves the new files — no restart needed
Runner details:
- Service:
forgejo-runner.serviceon boromir - Config:
/opt/scripts/.runner/config.yml - Registered to:
https://forgejo.exped.nu - Label:
boromir.ams1.nl.exped.systems - Executor:
host(runs directly on boromir, not in Docker)
To trigger a manual rebuild without a content change:
cd /var/www/domains/expeditioner.nl
git pull origin main
hugo --minify
To check runner status:
systemctl status forgejo-runner
To watch the runner logs:
journalctl -u forgejo-runner -f
Server setup
| Component | Details |
|---|---|
| Host | boromir.ams1.nl.exped.systems |
| Repo path | /var/www/domains/expeditioner.nl |
| Served from | /var/www/domains/expeditioner.nl/public |
| Web server | nginx |
| Nginx config | /etc/nginx/sites-enabled/expeditioner.nl |
| Front proxy | Nginx Proxy Manager on mithrandir.ams1 |
| TLS | Let's Encrypt via NPM |
| Domain | expeditioner.nu |
The nginx vhost config at /etc/nginx/sites-enabled/expeditioner.nl points directly at public/. A reference copy is kept in the repo root as nginx.conf for documentation purposes — changes to that file do not affect the live server automatically, you need to copy it manually if you update it.
Troubleshooting
Site not updating after a push
Check the Actions tab on the Forgejo repo to see if the workflow ran and whether it errored. Then check the runner logs:
journalctl -u forgejo-runner -f
Hugo build errors
Run manually to see the full error output:
cd /var/www/domains/expeditioner.nl
hugo --minify
Common causes: malformed front matter (YAML is whitespace-sensitive — use spaces, not tabs), an image path that doesn't exist, or a template syntax error if you edited a layout file.
Font not loading
Verify the file is in place:
ls -lh /var/www/domains/expeditioner.nl/static/fonts/
ls -lh /var/www/domains/expeditioner.nl/public/fonts/
If it's in static/ but not in public/, run hugo --minify again. If it's in both but still not rendering in the browser, check for a mixed-content warning in the browser console — the site must be served over HTTPS for the font to load.
Logo not showing
ls -lh /var/www/domains/expeditioner.nl/static/img/logo.png
File must be named exactly logo.png. If you use a different filename, update logoSrc in hugo.toml.
Accidentally broke a template
All layout files are in git — revert to the last working version:
git diff themes/ # see what changed
git checkout themes/ # revert all theme changes
hugo --minify