mirror of https://github.com/theNewDynamic/gohugo-theme-ananke.git

Patrick Kollitsch
yesterday 634cc090072fbc2f99eaef4a19639c016e3e245c
fix: make header height configurable (#972)

Makes the hero header height configurable via a new
`header_section_class` parameter, instead of hard-coding the vertical
padding in the header partials.

- Adds `header_section_class` to `layouts/_partials/site-header.html`
(home/list, both with and without a featured image) and
`layouts/_partials/page-header.html` (single page with a featured
image).
- Resolved with `.Param`, so it can be set site-wide under `[params]` or
overridden per page in front matter.
- Keeps the existing Tachyons defaults for each header, so current sites
render identically unless they opt in.

Closes #504

Documentation PR: gohugo-ananke/documentation#20

Adds a phase to `scripts/test-hugo-quickstart.ts` that builds two single
pages with featured images — one overriding `header_section_class` and
one using the default — and asserts the override is applied, does not
leak to other pages, and the historical default spacing (`tc-l pv6 ph3
ph4-ns`) is preserved.

Verified manually that:

- The site-wide param, per-page override, and `.Param` precedence all
resolve correctly across the site header (featured + non-featured) and
page header.
- Defaults are byte-for-byte unchanged when the parameter is unset.
- The new assertion fails if the partial regresses to a hard-coded
class.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3 files modified
142 ■■■■■ changed files
layouts/_partials/page-header.html 3 ●●●● patch | view | raw | blame | history
layouts/_partials/site-header.html 6 ●●●●● patch | view | raw | blame | history
scripts/test-hugo-quickstart.ts 133 ●●●●● patch | view | raw | blame | history
layouts/_partials/page-header.html
@@ -4,6 +4,7 @@
  {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}}
  {{ $featured_image_class := .Params.featured_image_class | compare.Default "cover bg-center" }}
  {{ $cover_dimming_class := .Params.cover_dimming_class | compare.Default "bg-black-60" }}
  {{ $header_section_class := .Param "header_section_class" | compare.Default "tc-l pv6 ph3 ph4-ns" }}
  {{ $responsive_image_widths := .Site.Params.ananke.responsive_image_widths | compare.Default (slice 480 960 1440 1920) }}
  {{ $responsive_header_id := "" }}
  {{ $background_image := $featured_image }}
@@ -31,7 +32,7 @@
  <header {{ with $responsive_header_id }}id="{{ . }}" {{ end }}class="{{ $featured_image_class }}" style="background-image: url('{{ $background_image }}');">
    <div class="{{ $cover_dimming_class }}">
      {{ partials.Include "site-navigation.html" . }}
      <div class="tc-l pv6 ph3 ph4-ns">
      <div class="{{ $header_section_class }}">
        {{ if not .Params.omit_header_text }}
          <div class="f2 f1-l fw2 white-90 mb0 lh-title">{{ .Title | compare.Default .Site.Title }}</div>
          {{ with .Params.description  }}
layouts/_partials/site-header.html
@@ -2,11 +2,12 @@
{{ if $featured_image }}
  {{/* Trimming the slash and adding absURL make sure the image works no matter where our site lives */}}
  {{ $featured_image_class := site.Params.featured_image_class | compare.Default "cover bg-top" }}
  {{ $header_section_class := .Param "header_section_class" | compare.Default "tc-l pv4 pv6-l ph3 ph4-ns" }}
  <header class="{{ $featured_image_class }}" style="background-image: url('{{ $featured_image }}');">
    {{ $cover_dimming_class := site.Params.cover_dimming_class | compare.Default "bg-black-60" }}
    <div class="{{ $cover_dimming_class }}">
      {{ partials.Include "site-navigation.html" .}}
      <div class="tc-l pv4 pv6-l ph3 ph4-ns">
      <div class="{{ $header_section_class }}">
        <h1 class="f2 f-subheadline-l fw2 white-90 mb0 lh-title">
          {{ .Title | compare.Default .Site.Title }}
        </h1>
@@ -22,7 +23,8 @@
  <header>
    <div class="pb3-m pb6-l {{ .Site.Params.background_color_class | compare.Default "bg-black" }}">
      {{ partials.Include "site-navigation.html" . }}
      <div class="tc-l pv3 ph3 ph4-ns">
      {{ $header_section_class := .Param "header_section_class" | compare.Default "tc-l pv3 ph3 ph4-ns" }}
      <div class="{{ $header_section_class }}">
        <h1 class="f2 f-subheadline-l fw2 light-silver mb0 lh-title">
          {{ .Title | compare.Default .Site.Title }}
        </h1>
scripts/test-hugo-quickstart.ts
@@ -711,6 +711,104 @@
}
/**
 * Sentinel CSS class used to verify the configurable hero header spacing.
 *
 * Issue #504: the height of the hero header is controlled by the
 * `header_section_class` parameter. The value is unique so it can only appear in
 * the output when the front matter override is honoured.
 */
const HEADER_SECTION_CLASS_MARKER = "ananke-header-test-pv7";
/**
 * Default header section spacing rendered by `page-header.html` for a single
 * page with a featured image when `header_section_class` is not set.
 */
const DEFAULT_PAGE_HEADER_SECTION_CLASS = "tc-l pv6 ph3 ph4-ns";
/**
 * Create two single pages that exercise the configurable header section class:
 * one overrides `header_section_class` in front matter, the other relies on the
 * theme default. Both set `featured_image` so the hero header branch renders.
 *
 * @param contentDir Absolute path to the project `content` directory.
 */
async function writeHeaderSectionClassFixtures(
    contentDir: string,
): Promise<void> {
    const overridePage = [
        "+++",
        "title = 'Custom Header Height'",
        "featured_image = '/images/custom-hero.jpg'",
        `header_section_class = '${HEADER_SECTION_CLASS_MARKER} ph3 ph4-ns'`,
        "+++",
        "",
        "Body.",
        "",
    ].join("\n");
    const defaultPage = [
        "+++",
        "title = 'Default Header Height'",
        "featured_image = '/images/default-hero.jpg'",
        "+++",
        "",
        "Body.",
        "",
    ].join("\n");
    await writeTextFile(join(contentDir, "custom-header.md"), overridePage);
    await writeTextFile(join(contentDir, "default-header.md"), defaultPage);
}
/**
 * Assert that the configurable `header_section_class` parameter is honoured on
 * hero headers and that omitting it keeps the historical default spacing.
 *
 * @param projectRoot Absolute path to the temporary quickstart project.
 * @throws Error when the override is dropped, leaks, or the default changes.
 */
async function assertHeaderSectionClassConfigurable(
    projectRoot: string,
): Promise<void> {
    const failures: string[] = [];
    const overrideHtml = await readTextFile(
        join(projectRoot, "public", "custom-header", "index.html"),
    );
    const defaultHtml = await readTextFile(
        join(projectRoot, "public", "default-header", "index.html"),
    );
    if (!overrideHtml.includes(HEADER_SECTION_CLASS_MARKER)) {
        failures.push(
            `- custom 'header_section_class' value '${HEADER_SECTION_CLASS_MARKER}' was not applied to the hero header`,
        );
    }
    if (defaultHtml.includes(HEADER_SECTION_CLASS_MARKER)) {
        failures.push(
            "- custom 'header_section_class' value leaked onto a page that did not set it",
        );
    }
    if (!defaultHtml.includes(DEFAULT_PAGE_HEADER_SECTION_CLASS)) {
        failures.push(
            `- default header section spacing '${DEFAULT_PAGE_HEADER_SECTION_CLASS}' was missing when 'header_section_class' was not set`,
        );
    }
    if (failures.length > 0) {
        throw new Error(
            [
                "Strict assertion failed: configurable header section class did not behave as expected.",
                "Failed assertions:",
                ...failures,
            ].join("\n"),
        );
    }
}
/**
 * Determine whether a directory is the work tree of a Git repository.
 *
 * @param path Absolute directory path.
@@ -1103,6 +1201,41 @@
        await assertDraftHiddenInProduction(projectRoot, homepagePath);
        console.log("[OK ] Production build should exclude draft content");
        console.log("\n[RUN] Configurable hero header section class (issue #504)");
        await writeHeaderSectionClassFixtures(join(projectRoot, "content"));
        const headerSectionBuildStep: StepDefinition = {
            name: "Build site with configurable header section fixtures",
            command: "hugo",
            args: [],
            cwd: projectRoot,
            expectedFiles: [
                "public/custom-header/index.html",
                "public/default-header/index.html",
            ],
        };
        const headerSectionBuildReport = await executeHugoBuildStep(
            headerSectionBuildStep,
            projectRoot,
        );
        reports.push(headerSectionBuildReport);
        if (options.verbose) {
            console.log(
                `      ${formatCommand(headerSectionBuildStep.command, headerSectionBuildStep.args)}`,
            );
            console.log(
                `[OK ] ${headerSectionBuildStep.name} (${headerSectionBuildReport.result.durationMs} ms, exit ${String(headerSectionBuildReport.result.code)})`,
            );
            const trimmedOutput = headerSectionBuildReport.result.combined.trim();
            if (trimmedOutput) {
                console.log(trimmedOutput);
            }
        }
        await assertHeaderSectionClassConfigurable(projectRoot);
        console.log("[OK ] Configurable hero header section class (issue #504)");
        console.log("\nResult: PASS");
        if (options.keepOnSuccess) {