Rebuilding an Artist’s Portfolio with Hugo
My brother Nils Karsten is an artist based in New York. For years his website ran on Indexhibit, a classic portfolio CMS that had become tied to PHP 7.4. It received no updates, its markup was dated and it offered almost no SEO. Rather than patch it again, I rebuilt the site from scratch as a static site with Hugo Extended (0.168). It has been live at nilskarsten.com since the end of October 2026.
Why static
A portfolio changes a few times a month, not a few times a minute. With Hugo there is no database and no PHP runtime
left to maintain, and the attack surface shrinks to the web server itself. Every artwork series lives in a page bundle
together with its images. Hugo’s image processing generates responsive WebP variants (480 to 1600 px, plus a larger one
for the lightbox) with srcset/sizes at build time, so the originals from the studio can go in unchanged.
All addresses of the old site were kept – only the /index.php part is gone, and the old URLs redirect with a 301.
One page, many tags
The woodcut work used to be split into several sub-series. The artist preferred a single page arranged by visual
relationships instead of by theme. So every image now carries tags in its front matter (several per work are allowed),
and a small client-side filter written without any library shows or hides the works, loading more in steps of 24.
The active filter is kept in the URL (/work/woodcuts/#history), so filtered views can be linked. The old series URLs
redirect to the matching filter with a 301 in .htaccess. A parameter in hugo.toml switches the filter on or off.
Environments instead of manual edits
There are two targets: a staging server for reviews and the live site. Staging is built with --environment staging,
which sets noindex on every page and writes a robots.txt with Disallow: /. Production allows indexing and writes
the sitemap. Features such as the randomly chosen hero image on the homepage were first enabled on staging only and went
live through the configuration, without any template changes.
The builds go to separate folders:
|
|
There is a reason for this. Early on, a running hugo server silently overwrote public\ with a build pointing to
the local preview address, and that build got uploaded. Since then public\ is used only for the local preview, and the upload is a
WinSCP sync from _live\ with removal of stale files. Before each upload, a short check counts references to the
preview address and the staging domain in the output (both must be zero).
SEO
descriptionand SEO title per page, JSON-LD for the artist and the works- Alt texts generated from the work title and caption
- Descriptive image filenames and an image sitemap with
lastmod og:image,twitter:card, the hero image loaded eagerly, a 404 page without canonical- HTTPS redirect and Brotli compression handled by the host
Matching images against the catalogue
The artist keeps a catalogue of works as a PDF. To match the website images with their titles and dimensions, I
extracted the images with pdfimages and the captions with pdftotext. The two were then matched with OpenCV feature
matching (ORB + RANSAC). Real matches had hundreds of inliers and false matches fewer than 50, which made the assignment
straightforward.
Benefits
- No server-side runtime, no CMS updates, minimal attack surface
- Fast pages with responsive WebP images
- A safe review loop through a separate staging build
- Linkable filtered views instead of rigid sub-series
- Clean metadata for search engines and social previews