Website for Expeditioner
  • CSS 44.5%
  • HTML 31.8%
  • PHP 23.7%
Find a file
nhendriks d7a672d6e8
All checks were successful
Build and deploy / deploy (push) Successful in 3s
Document EXIF stripping and the fixed background layer
2026-10-16 11:47:07 +02:00
.forgejo/workflows feat: add sudo to privileged commands for forgejo runner 2026-04-07 22:26:24 +02:00
content Centre work page text and lower the page photos 2026-10-11 20:35:52 +02:00
static Smaller portrait and roadtrip photos without EXIF 2026-10-16 09:52:10 +02:00
themes/expeditioner Fixed background layer and a smaller hero photo without EXIF 2026-10-16 09:49:20 +02:00
.gitignore Initial commit of all files 2026-03-29 07:54:27 +02:00
.hugo_build.lock Published Hugo 2026-03-29 08:08:17 +02:00
hugo.toml Update text at contact form 2026-03-31 20:13:26 +02:00
README.md Document EXIF stripping and the fixed background layer 2026-10-16 11:47:07 +02:00

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

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.nl sender - a free relay pointed at third parties and an easy way to burn the domain's reputation.
  • A newline in name is fatal. That field goes into a Reply-To header, and h() 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:

  1. Forgejo runner on boromir picks up the job
  2. Pulls latest from main into /var/www/domains/expeditioner.nl
  3. Runs hugo --minify to rebuild public/
  4. Nginx immediately serves the new files — no restart needed

Runner details:

  • Service: forgejo-runner.service on 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