From WordPress to Hugo: How BusinessCode completely rethought its website

10 min Goetz van Rissenbeck

Three stages, one goal

The BusinessCode website went through three stages:

  1. WordPress at a hosting provider – with the provider’s template system
  2. Hugo + Hextra – the first step towards a static site generator, still Bootstrap‑based
  3. Hugo + custom BCD theme – Tailwind CSS, Bento layout, glassmorphism and DecapCMS

Each stage solved problems from the previous one – and opened up new possibilities. In this article we describe the entire journey, the decisions behind it and what makes Hugo so powerful when combined with modern tooling.


Stage 1: WordPress at the hosting provider

The old site ran on WordPress at a hosting provider. Design and layout came via the provider’s template system – a ready‑made solution that looked fine at first glance but constantly caused problems in practice:

  • Images were cropped incorrectly – the template system used fixed aspect ratios that did not match the content
  • Layout instability – columns, spacing and font sizes behaved unpredictably depending on the content
  • Gutenberg → Atomic Blocks – as WordPress moved to block‑based editors, the site became increasingly fragile; layouts that worked in the old editor fell apart in the new one
  • No version control – changes to content and templates were hardly traceable
  • No CI/CD – deployments were FTP or backup restore scenarios
  • Multilingual support (DE/EN) via plugin, often fragile in the details

The site increasingly no longer looked the way it was supposed to – and every WordPress update made things worse rather than better.


Stage 2: Hugo + Hextra (Bootstrap)

The first step away from WordPress led to Hugo as a static site generator with the Hextra theme, which is based on Bootstrap.

This solved the core problems:

  • Static delivery – no PHP runtime, no database, no security updates in production
  • Git as single source of truth – content and templates finally version‑controlled
  • Fast builds – Hugo generates the entire site in seconds

But Hextra brought its own limitations:

  • Bootstrap overhead – the entire framework was loaded although only a fraction was used
  • Hard to customise – design changes required overriding Bootstrap classes and variables
  • No custom design system – the site looked like one of many Bootstrap sites
  • Dark mode was not part of the plan

Hextra was the right step at the right time – but it was not enough for a truly distinctive look and feel.


Stage 3: Custom theme with Tailwind CSS

In the second Hugo iteration we replaced Hextra entirely and built a custom theme (BCD) based on Tailwind CSS – with a Bento layout and glassmorphism as the consistent design language.

Why Tailwind instead of Bootstrap?

  • Utility‑first – no more overriding framework classes; direct styling in the markup
  • Tree‑shaking – only the classes actually used end up in the build; the CSS is tiny
  • Dark mode as a core feature – via the class strategy and CSS custom properties
  • No CDN dependency – everything is compiled and served locally

Glassmorphism as the design language

The central visual element is a consistent glassmorphism system:

.glass {
  background: var(--glass-bg);
  backdrop-filter: blur(20px) saturate(1.8);
  border: 1px solid var(--glass-border);
  box-shadow: var(--glass-shadow);
  border-radius: 1.25rem;
}

CSS custom properties switch values automatically between light and dark mode:

:root {
  --glass-bg: rgba(255, 255, 255, 0.6);
  --glass-border: rgba(22, 22, 105, 0.08);
}
.dark {
  --glass-bg: rgba(13, 13, 55, 0.55);
  --glass-border: rgba(54, 152, 250, 0.12);
}

This .glass class runs through the entire site: navigation, cards, search modal, blog navigation – all glassy, all consistent.

PostCSS pipeline instead of SCSS

Tailwind is compiled via Hugo’s built‑in PostCSS integration:

{{/* head/css.html */}}
{{ $css := resources.Get "css/main.css" }}
{{ $css = $css | css.PostCSS }}
{{ if hugo.IsProduction }}
  {{ $css = $css | minify | fingerprint }}
{{ end }}

The result: a single, fingerprinted CSS file with subresource integrity – no external dependencies.

WCAG compliance

Accessibility is baked into the base CSS:

  • :focus-visible styles for keyboard navigation
  • prefers-reduced-motion media query disables animations
  • Semantic HTML throughout all layouts and partials

DecapCMS: Editorial work without WordPress

A static site generator alone is not enough – editors need an interface.
For this we use DecapCMS (formerly Netlify CMS).

How it works

DecapCMS runs as a single‑page app at /admin/ and talks directly to the GitLab repository:

  • Authentication via GitLab OAuth with PKCE flow – no separate auth server needed
  • Editorial workflow – changes create merge requests that can be reviewed
  • Multilingual content – DE and EN are managed as separate files in the same bundle
  • Media is stored relative to the content bundle

Local development without login

So that developers can test DecapCMS locally without logging into GitLab, the admin page automatically detects localhost:

var isLocal = location.hostname === 'localhost'
           || location.hostname === '127.0.0.1';
if (isLocal) {
  CMS.init({
    config: {
      local_backend: true,
      backend: { name: 'git-gateway' }
    }
  });
}

make serve starts the Hugo server and the DecapCMS local backend proxy automatically:

make serve
# ✅ DecapCMS local backend running on http://localhost:8081
# ✅ Hugo server running at http://localhost:1313

No manual toggling of config flags, no separate process.

Collections for blog, news and projects

The config.yml defines collections for all content areas:

  • Blog and News – with fields for title, summary, author, categories, tags, hero images and external links
  • Projects – with nested folders (up to 3 levels deep), category select, weight and icon path

Editors see exactly the fields defined in the front matter – nothing more, nothing less.


The developer workflow

One command for everything

The entire development setup is abstracted through a Makefile:

make serve        # Start Hugo + DecapCMS locally
make stop         # Stop everything
make test         # Run E2E tests
make validate     # Content validation
make build        # Production build
make new-blog SLUG=my-article  # Create a new blog bundle

The Makefile automatically detects whether Podman, Docker or a native Hugo installation is available.
This gives everyone on the team:

  • the same Hugo version
  • the same build process
  • identical behaviour to CI

GitLab CI/CD

On the main branch the following runs automatically:

  1. Content validation – checks for missing translations, icon references, hero images, front matter consistency
  2. Hugo build – with minification and resource caching
  3. Deployment to GitLab Pages

If validation fails, the site is not deployed. On feature branches Hugo builds with --buildDrafts so that drafts can be previewed.


Hugo features that make the difference

Responsive images with AVIF + WebP

Hugo processes images at build time. Our responsive-picture partial generates a complete <picture> element:

<picture>
  <source type="image/avif" srcset="...400w, ...600w, ...800w, ...1200w" />
  <source type="image/webp" srcset="...400w, ...600w, ...800w, ...1200w" />
  <source srcset="...400w, ...600w, ...800w, ...1200w" />
  <img src="fallback-800.webp" loading="lazy" decoding="async" />
</picture>

AVIF, WebP and original format are generated in four widths – automatically, for every image on the site.

Local search with Alpine.js

Full‑text search runs entirely client‑side – no external service, no server:

  • Hugo generates a JSON index of all content at build time
  • Alpine.js renders the search modal with live results
  • Keyboard shortcuts: Ctrl+Shift+K to open, arrow keys to navigate
  • Results are categorised by type (blog, news, services, projects, guides)
  • All labels come from i18n keys – the search works in every language

Shortcodes for recurring patterns

Instead of formatting content manually, editors use Hugo shortcodes:

  • gallery / gallery-item – image gallery with modal, keyboard navigation and responsive thumbnails
  • hero-image – floating hero image with text wrap and caption
  • tech-icon / tech-icons – technology and expertise icons as SVG grids
  • callout – highlighted info boxes
  • button – styled links as buttons
  • terminal – terminal output in code block style
  • customer-logos – customer logos from data/references/

SEO as a core feature

SEO is not an afterthought but built in:

  • seoTitle front matter – allows shorter <title> tags independent of the page title
  • Smart title logic – if seoTitle + site suffix > 60 characters, the suffix is dropped
  • Hreflang tags with x-default for the default language
  • Structured data (Schema.org) for Organization, Article, BreadcrumbList
  • OpenGraph & Twitter Cards via front matter parameters
  • SEO optimizer script – crawls the live site and reports missing meta tags, titles that are too long, missing descriptions

Multilingual support throughout

Hugo’s multilingual support is not a plugin but a core feature:

  • File‑based translations: index.de.md, index.en.md
  • i18n files for all UI strings (i18n/de.yaml, en.yaml)
  • Automatic language switcher with country flags
  • Permalinks configurable per language (/projekte/:slug/ vs. /projects/:slug/)
  • Content validator checks that all translations are present and consistent

29 E2E tests with Playwright

Frontend quality is not checked by eye but automated:

tests/e2e/
├── navigation.spec.ts          # Main menu, submenus, mobile
├── search.spec.ts              # Search modal, results, keyboard
├── theme-switching.spec.ts     # Dark/light mode
├── seo-compliance.spec.ts      # Meta tags, H1, hreflang
├── gallery.spec.ts             # Image gallery with modal
├── blog-layout.spec.ts         # Blog pages, pagination
├── contact.spec.ts             # Contact form, honeypot
├── image-srcset.spec.ts        # Responsive images
├── rss-sitemap.spec.ts         # RSS feed, sitemap
├── ... and 20 more

Tests run containerised via Podman – locally and in CI with an identical setup:

make test
# or targeted:
npm run test:podman:rebuild -- --project=chromium navigation.spec.ts

Performance optimisation: Lighthouse 75 → 99

Beyond functionality and design, we specifically optimised the site for web performance – measured with Google Lighthouse.

Starting point

An initial Lighthouse audit on a blog page returned:

Category Score
Performance 75
Accessibility 100
Best Practices 100
SEO 100

The main reason for the low Performance score: a Cumulative Layout Shift (CLS) of 0.577 – almost six times the recommended threshold (< 0.1). The entire page visibly shifted as soon as the web font loaded.

The three levers

1. Font loading without layout shift

The problem: font-display: swap shows the system font first, then swaps in Titillium Web. Because both fonts have different metrics (character width, ascenders and descenders), the entire page content shifts.

The solution: a metrics‑matched fallback font that occupies exactly the same space as Titillium Web:

@font-face {
  font-family: 'Titillium Web Fallback';
  src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
  ascent-override: 106%;
  descent-override: 34%;
  size-adjust: 97%;
}

Additionally, the two most‑used font weights (Regular 400 and Semibold 600) are preloaded via <link rel="preload">, so the swap is often not visible at all.

Result: CLS from 0.577 → 0.

2. Don’t lazy‑load the first image

Hugo’s image processing sets loading="lazy" on all images by default. On blog pages, the first image is often above the fold and thus the Largest Contentful Paint (LCP) element. Lazy loading delays rendering here.

Using a counter in the render hook, the first image in the content automatically gets loading="eager", while all subsequent images remain lazy:

{{ $imgCount := .Page.Store.Get "render-image-count" | default 0 }}
{{ .Page.Store.Set "render-image-count" (add $imgCount 1) }}
{{ $loading := cond (eq $imgCount 0) "eager" "lazy" }}

3. Pre‑compression in the CI pipeline

GitLab Pages automatically serves pre‑compressed files when .br or .gz variants are present. In the CI pipeline, all text assets are compressed with Brotli and Gzip after the Hugo build:

- find public -type f -regex '.*\.\(html\|css\|js\|svg\|json\)$' -exec gzip -f -k {} \;
- find public -type f -regex '.*\.\(html\|css\|js\|svg\|json\)$' -exec brotli -f -k {} \;

Result

Metric Before After
Performance Score 75 99
CLS 0.577 0
Speed Index 2.5 s 2.4 s
LCP 1.4 s 1.5 s
FCP 0.9 s 0.9 s

What we gained

For design

  • Tailwind instead of Bootstrap – utility‑first, no framework overrides, tiny CSS
  • Glassmorphism & dark mode – consistent throughout, controlled via CSS variables
  • Responsive images – AVIF/WebP automatic, no manual optimisation
  • Zero CDN – fonts, JS, CSS – all local, privacy‑compliant

For editors

  • DecapCMS – a familiar interface for creating and editing content
  • Editorial workflow – changes as merge requests, with review option
  • Local previewmake serve starts everything, including the CMS
  • Content validator – catches errors before they go live

For developers

  • Everything version‑controlled – content, layouts, tests, validation logic
  • Container workflow – identical environment locally and in CI
  • 29 E2E tests – regressions become visible immediately
  • One Makefile – one command for serve, test, build, validate

For the future

  • New content is created as Markdown bundles with a clear structure
  • New shortcodes extend what editors can do
  • Static delivery makes the site fast, secure and cost‑effective to run

Conclusion

The BusinessCode website went through three stages – each one moved the project forward:

  • WordPress → Hugo: away from a fragile, increasingly broken hosting solution towards static delivery and Git as single source of truth
  • Hextra → custom BCD theme: away from Bootstrap overhead towards Tailwind CSS with a Bento layout and glassmorphism
  • Manual editing → DecapCMS + CI/CD: away from FTP deployments towards a review workflow with automatic validation

Hugo proved to be a powerful toolkit across both iterations: multilingual support, image processing, taxonomies and a flexible template engine – all out of the box. Combined with Tailwind CSS for the design, DecapCMS for the editorial team and GitLab CI/CD for operations, the result is a system that looks modern, feels robust and is transparent and manageable for everyone involved.