ScreenshotNeo

BlogHow-to

How to Run Visual Screenshot Tests for a Static Site Built with Hugo

Build your Hugo site, compare its pages with Playwright screenshot baselines, and run the checks in CI with fewer noisy diffs.

By the ScreenshotNeo team4 October 20269 min read

Build the Hugo site, serve its generated files, and use Playwright Test’s toHaveScreenshot() matcher to compare browser screenshots with committed reference images. Keep the viewport, browser, and operating system stable between baseline creation and CI runs. When a comparison changes, inspect the diff before updating the reference.

Hugo creates the pages; Playwright renders those pages in a browser and checks whether their pixels changed. This guide tests the generated output, which is useful when you want to check what Hugo actually builds. Hugo documents the hugo build command and the hugo server command separately. Use the development server only when its behavior is what you intend to test.

1. Install Playwright Test

From the Hugo project root, initialize npm if the project does not already have a package.json, then install Playwright Test and its Chromium browser:

npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium

Commit package.json and the lockfile so local and CI installations use the same dependency versions. If the project already uses another package manager, use its equivalent install commands and lockfile.

2. Build and serve Hugo’s generated output

Check the project configuration to find its destination directory. Hugo commonly writes to public/, but a project can configure a different destination. Build the site, then serve that directory with a static file server:

hugo build
python3 -m http.server 1313 --directory public

Keep the static server running in one terminal. In a second terminal, open http://127.0.0.1:1313/ and confirm that the expected page loads. If the generated files are in a different directory, substitute that path. If the site is built under a subpath or has a specific baseURL, configure Hugo and the test URL to match it.

3. Configure Playwright to start the site server

Playwright’s webServer setting can build Hugo and serve the output before the browser tests run. Create playwright.config.ts in the repository root:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests/visual',
  fullyParallel: true,
  reporter: 'list',
  use: {
    baseURL: 'http://127.0.0.1:1313',
    ...devices['Desktop Chrome'],
    viewport: { width: 1280, height: 800 },
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
  },
  expect: {
    toHaveScreenshot: {
      animations: 'disabled',
      caret: 'hide',
    },
  },
  webServer: {
    command: 'hugo build && python3 -m http.server 1313 --directory public',
    url: 'http://127.0.0.1:1313/',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

This example assumes Hugo is on PATH, Python 3 is installed, and Hugo outputs to public/. Update the build command, destination, and base URL to fit the repository. The server readiness URL should return successfully once the server starts. In CI, Playwright starts a fresh server; locally it can reuse an already running one.

4. Write a screenshot test

Create tests/visual/pages.spec.ts. Start with a small group of representative templates: the home page, a typical content page, and a list or archive page. Replace the example paths with pages that exist in your Hugo project.

import { test, expect } from '@playwright/test';

test('home page visual baseline', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('home.png');
});

test('article page visual baseline', async ({ page }) => {
  await page.goto('/posts/example/');
  await expect(page).toHaveScreenshot('article.png');
});

test('section list visual baseline', async ({ page }) => {
  await page.goto('/posts/');
  await expect(page).toHaveScreenshot('posts-list.png');
});

Run the tests with:

npx playwright test

On the first run, Playwright creates reference screenshots. Review them and commit the snapshot files alongside the tests. Later runs compare the rendered page with those references. Playwright’s visual comparisons documentation describes the matcher, snapshot behavior, and options.

5. Keep comparisons deterministic

A screenshot comparison checks rendered pixels, so unrelated differences can make a test fail. Playwright warns that output can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Use the same environment for baseline updates and CI comparisons where possible.

  • Fix the viewport. Set the same width and height for baseline creation and comparison. Add separate tests only for responsive sizes that matter.
  • Pin the browser environment. Use the same Playwright dependency and browser installation in baseline work and CI. If you deliberately compare multiple browsers or operating systems, treat each as its own baseline configuration.
  • Use stable page content. Avoid dates, rotating content, random values, and remote data that changes between runs. Prefer fixed fixtures and assets served by the generated site.
  • Wait for meaningful readiness. If a page has client-side rendering or delayed assets, wait for a specific element or an application-ready condition before taking the screenshot. Avoid arbitrary sleeps unless the page offers no better readiness signal.
  • Control animation and caret noise. The configuration above disables animations and hides the text caret for the screenshot assertion.
  • Filter only truly volatile regions. Playwright supports a stylePath option to inject CSS during screenshot capture. Use a narrow stylesheet for content that cannot be stabilized, and document what it hides. A broad mask can conceal a real regression.
  • Set a difference tolerance only after reviewing diffs. The maxDiffPixels option allows some pixels to differ. Do not use a generous global threshold to silence unexplained changes.

Example of a per-assertion tolerance, if a reviewed comparison shows a small, known amount of rendering drift:

await expect(page).toHaveScreenshot('home.png', {
  maxDiffPixels: 20,
});

The value is illustrative, not a recommended universal threshold. Choose it based on actual diffs in your environment.

6. Run the tests in CI

A CI job should install the project’s pinned dependencies, install the Playwright browser, build and serve Hugo output, and run the same command used locally. The following GitHub Actions workflow assumes Hugo is already installed on the runner and is available on PATH; add your project’s Hugo installation step if that is not true.

name: Visual tests

on:
  pull_request:
  push:
    branches: [main]

jobs:
  screenshots:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      # Install Hugo here using the version and installation method
      # selected by your project.

      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test

GitHub Actions runs repository workflows on hosted or self-hosted runners; see the GitHub Actions documentation. For repeatable snapshots, keep the runner operating system and browser consistent with the environment used to approve baselines. Pin action versions according to your repository’s maintenance policy.

When a test fails, preserve the Playwright report and any screenshot diff artifacts using the artifact workflow your repository already uses. Review the failed image and expected image before changing reference files. Do not update snapshots automatically after every CI failure: that can approve an unintended regression without review.

7. Update baselines deliberately

If a visual change is intentional, inspect the current screenshot and diff, then regenerate references:

npx playwright test --update-snapshots

Review the changed snapshot files with the code change and commit them. A changed image is a signal for review, not proof of a defect. If the change was accidental, fix the site and rerun the test without updating the approved reference.

8. Add coverage without making the suite noisy

  • Cover one representative route for each materially different Hugo layout before adding every content URL.
  • Use descriptive names tied to page purpose and viewport. Playwright stores snapshots alongside the test in a test-specific snapshot directory.
  • Add a mobile viewport when the responsive layout is important; every additional viewport and browser creates more references to review.
  • Keep snapshot assertions focused. A separate test per route makes it easier to tell which page changed.
  • Consider element screenshots when only one component matters. Page screenshots are better for layout and page-level changes; a targeted element assertion limits unrelated page content.

9. Troubleshooting

Symptom Likely cause Fix
Connection refused or webServer timeout The command did not start, the port is occupied, or the readiness URL is wrong. Run the command manually, check Hugo’s build output, confirm the generated directory, and use the same host and port in baseURL and webServer.url.
404 on a route The test path does not match Hugo’s generated URL, or the site uses a base path. Inspect the generated output and site configuration, then correct the route and server URL.
Every screenshot differs in CI The baseline and CI use different operating systems, browser versions, fonts, or rendering settings. Regenerate and compare baselines in the CI environment, or deliberately maintain distinct baselines for distinct environments.
Only text or a small region differs Font availability, dynamic content, time-dependent data, or animation may vary. Install the required fonts, stabilize the content, disable motion for the assertion, or filter only the specific volatile region.
Screenshot is captured before content appears Navigation completed before client-side content or lazy assets finished loading. Wait for a stable selector or an application-ready signal before asserting the screenshot.
Snapshots are missing or named unexpectedly The test path, project configuration, or snapshot naming differs from the expected setup. Check the Playwright report and snapshot directory, and use explicit stable names in toHaveScreenshot().
Updating snapshots produces many changes The environment or the underlying site changed broadly. Do not commit the whole update blindly. Compare representative pages, identify the shared cause, and update only after the visual changes are understood.
Browser installation fails in CI The browser binaries or system dependencies are missing. Install the browser for the configured Playwright version; on Linux, npx playwright install --with-deps chromium installs browser dependencies as well.

10. Performance, reliability, and cost

Screenshot tests cost CI time and snapshot review effort. Start with the smallest route set that covers the distinct templates, and add routes when they catch a meaningful class of change. Multiple browsers, viewports, and routes multiply browser work and baseline maintenance.

Reliability depends more on stable inputs and a consistent rendering environment than on a large pixel tolerance. Local Playwright baselines are version-controlled files, so they add repository storage and need review when changed. CI execution also consumes runner time. The workflow does not require a particular deployment host: test the generated files locally to keep the check separate from deployment behavior.

11. Local Playwright or hosted visual review?

Playwright’s built-in snapshots are a good starting point when the team wants tests and baselines in the repository. A hosted review service is optional if a dedicated visual review interface better fits the team’s process.

  • Local Playwright: reference images live with the code, and diffs are reviewed through the project’s normal code review. The team owns browser and environment consistency.
  • Chromatic: its official Playwright guide describes uploading page archives and reviewing cloud snapshots; it states support for Playwright 1.38.0 and above.
  • Percy: its official Playwright client documentation describes a percySnapshot flow and CLI integration with Playwright screenshot assertions.

Evaluate a hosted workflow against baseline ownership, review process, environment management, and setup overhead. The cited product documentation establishes integration behavior, not comparative quality or current commercial terms. Neither hosted service is required to test a Hugo site.

Or skip the browser setup

ScreenshotNeo is a screenshot API and MCP server. For a one-off capture of a page from the built Hugo site that is publicly reachable, call the API directly:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/ -o shot.webp

See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for ScreenshotNeo’s free plan.

FAQ

Should visual tests target hugo server or generated files?

Use generated files when you want to test the build output as a static site. Use hugo server when testing its development-server behavior is part of your goal.

Do screenshot tests replace functional tests?

No. They detect rendered appearance changes. Keep functional checks for behavior such as navigation, forms, and interactions.

Should I test every page Hugo generates?

Usually begin with representative routes for each template and add individual pages when their content or layout warrants separate coverage.

Can I use these checks without GitHub Actions?

Yes. Run the same Hugo build and Playwright command in any CI system that can install the project’s dependencies and browser.