This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Hugo static site for lna-dev.net (personal site of Lukas Nagel). Theme is hugo-PaperMod, pulled in as a Hugo Module via go.mod (no submodules). Trilingual: English (default), German, Swedish — each language gets its own subdirectory in URLs (defaultContentLanguageInSubdir: true).
- Local build:
hugo— output goes topublic/. - Local server:
hugo server -D(drafts on). - Deploy:
./deploy.shbuilds twice (clearnet + Tor onionbaseURL) and rsyncspublic/to the production server. Do not run this without the user's say-so — it pushes live. - Module bootstrap (only if
go.sumis missing/broken):go mod download. - Image binaries are intentionally not in git and live outside the repo. Hugo mounts them in via
module.mountsinhugo.yaml: the gallerysourceis the repo-rootgallery-photossymlink (gitignored) → the local photo store, mapped to theassets/images/gallery/virtual path. Per machine, create the symlink once:ln -s <photo-store> gallery-photos. A clean checkout without it builds fine but renders an empty gallery. (Hugo ignores symlinks during resource discovery — which is why the oldassets/images/gallerydirectory symlink silently produced an empty gallery — but it does resolve one given explicitly as a mountsource. See memoryproject_gallery_mount.md.)
The gallery was recently restructured (see memory project_gallery_overhaul.md). The model is one flat image folder + central YAML metadata + theme dispatch. Understanding this is essential before touching any gallery template.
- Image source of truth: the local photo store, mounted to the
assets/images/gallery/virtual path (via thegallery-photossymlink +module.mounts— see Build / Deploy). All photos sit flat there as global resources regardless of which view they appear in; templates address them asimages/gallery/*, unaffected by where the files physically live. - Metadata source of truth:
data/gallery.yaml— one entry per image withid(UUID, stable across renames),src(current filename),category,section(general|archive),project(slug or empty),portfolio(bool),tags,alt,title,license,artist. Thesrcfield must exactly match the image filename. Theidis the stable identifier used by deep links, the likes API, andfeatured_image:references — when a filename changes, onlysrcis updated;idstays. - Two cached partials underpin everything else:
layouts/partials/gallery-images.html— returns all gallery image resources viaresources.Match "images/gallery/*". Global resources are inherently language-independent. Always call viapartialCached. Note:.Nameon these resources returns a full path likeimages/gallery/photo.jpg— usepath.Basewhen looking up by bare filename.layouts/partials/gallery-meta.html— returns{ bySrc, byId }, two dicts sharing the same metadata entries.bySrcis keyed by bare filename (usepath.Baseon resource.Nameto match);byIdis for UUID lookups (featured_image, etc.). Hard-fails the build if any entry lacks anid— runscripts/add-gallery-ids.pyto populate one. Always call viapartialCached.
layouts/partials/collect-images.htmlis the single entry point for collecting+filtering images. It merges YAML metadata with EXIF/XMP, resolves license viadata/licenseMap.yaml(errors hard if a license string isn't mapped), and resolves artist (data → EXIF →site.Params.author.name). Filter keys:category,section,portfolio,project,excludeArchive(default true). Each returned item exposes.Id(UUID from YAML) — use it as the stable identifier in any new template. Use this — do not iterate the bundle directly.layouts/partials/resolve-featured-image.htmlresolves afeatured_image:front-matter value (UUID, with legacy filename fallback) to a bundle resource. Used byget-gallery.htmlandtemplates/_funcs/get-page-images.html.- Theme dispatch: gallery pages select a renderer through their front-matter
params.theme, branched inlayouts/_default/list.htmlandlayouts/_default/single.html. Themes:gallery-home→content/gallery/_index.*— hero portfolio + nav cards to Projects/General + Archive link.gallery-general-home→content/gallery/general/_index.*— all non-archive images with category filter bubbles (gallery-filter-bubbles.html, JS-only client-side filtering ondata-category).gallery-archive→ archive view (forcessection=archivefilter).gallery-portfolio→ curated subset (portfolio: true).gallery-projects-list→ index of project mini-albums.gallery-project→ single project album (filters byproject_slug).gallery-page/gallery-page-hidden→ generic gallery single pages.
- Lightbox: PhotoSwipe, wired up in
assets/js/lightbox.js; thumbnail and full-size images are generated by Hugo image processing ingallery.html(600px and 4000px fits).
content/projects/index.{en,de}.md lists the user's software projects in front-matter (projects: array). Rendered by layouts/projects/single.html, which fetches live GitHub stats per repo/org via resources.GetRemote against the GitHub API (allowed by the security.http.urls whitelist in hugo.yaml). Don't confuse these with gallery projects/ (photo series).
content/feed/_index.* + layouts/feed/list.html builds a unified activity timeline by merging:
- All
RegularPagesin thepostssection across all languages (lang.Merge), - Photos from the gallery headless bundle (filtered to non-archive, dated >2000),
- Talks from
data/talks.yaml.
Items are grouped by year/month and filterable client-side. Photo permalinks use #<FNV32a hash of RelPermalink> so they jump to the right gallery item — gallery.html writes the matching data-id attribute. Keep this hash scheme consistent if you change either side.
Custom layouts/_default/rss.xml. The gallery overhaul preserves RSS URL stability — when moving images around, generate redirects rather than breaking old item links.
UI strings in i18n/{en,de,sv}.yaml. Per-language menus, descriptions, and home content live under languages.<lang>.params in hugo.yaml. Content files use index.<lang>.md for branch bundles and _index.<lang>.md for sections.
- Use Hugo image processing (
images.Process,images.AutoOrient) — never reference raw image URLs directly, and alwaysAutoOrientfirst so EXIF rotation is applied. - New gallery images: drop the file into the photo store (the
gallery-photosmount target), then add a matching entry todata/gallery.yamlincluding a UUIDid(the build will hard-fail without one —scripts/add-gallery-ids.pypopulates missing ids in bulk). - Renaming an existing gallery image: rename the file in the photo store (
gallery-photostarget) and update only the matchingsrc:indata/gallery.yaml. Do not touchid— deep links, the likes API key, and anyfeatured_image:referencing it all use the id. - New license values must be added to
data/licenseMap.yamlor the build willerrorf. - Gallery scripting/migration helpers are written in Python (per project memory).