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

Patrick Kollitsch
2 days ago c88d2fc98c6b84d847f06b5f1b644d6dadb2299a
fix: allow images in list summary cards (#971)

## Summary

Lets list pages opt in to image summary cards, and makes published dates
consistent across single pages and summary cards.

- Adds an opt-in `params.ananke.pages.show_list_images` switch. When
enabled, list pages render the existing `summary-with-image` card
instead of the plain `summary` card. Default is off, so existing sites
are unchanged.
- Applies the existing `params.ananke.pages.show_date` setting (with the
per-page `ananke.show_date` override) to summary cards so dates display
consistently with single pages.
- Extracts the shared date-visibility logic — previously duplicated in
`single.html` — into a single `layouts/_partials/func/ShowDate.html`
partial used by `single.html`, `summary.html`,
`summary-with-image.html`, and `post/summary.html`.

Closes #217.

## Documentation

Documentation PR: gohugo-ananke/documentation#13

## Scope notes

- Taxonomy term pages (e.g. `/tags/foo/`) are list pages that fall back
to `list.html`, so they pick up image cards from the same switch — no
separate template change needed.
- `summary-with-image` is also used by the home page recent-posts
section, so enabling dates means those cards now show dates by default
(controllable via `show_date`). This is intentional and documented, but
it is a visible default change worth a maintainer's eye before merge.

## Testing

Adds a phase to `scripts/test-hugo-quickstart.ts` that builds a content
section twice:

1. Defaults — asserts no image cards and that dates show.
2. `show_list_images = true` + `show_date = false` — asserts image cards
render and dates are hidden, proving the two settings are independent.

Verified manually that:

- Image cards appear on section lists and taxonomy term lists when
enabled, and never by default.
- Dates show by default and respect `ananke.pages.show_date = false`
plus the per-page `ananke.show_date = true` opt-back-in, on both summary
cards and single pages.
- `single.html` output is unchanged by the partial extraction.
- The new assertions fail if the image switch or the date logic
regresses.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7 files modified
1 files added
254 ■■■■■ changed files
layouts/_partials/func/ShowDate.html 31 ●●●●● patch | view | raw | blame | history
layouts/list.html 7 ●●●● patch | view | raw | blame | history
layouts/post/list.html 7 ●●●● patch | view | raw | blame | history
layouts/post/summary.html 9 ●●●●● patch | view | raw | blame | history
layouts/single.html 14 ●●●●● patch | view | raw | blame | history
layouts/summary-with-image.html 6 ●●●●● patch | view | raw | blame | history
layouts/summary.html 6 ●●●●● patch | view | raw | blame | history
scripts/test-hugo-quickstart.ts 174 ●●●●● patch | view | raw | blame | history
layouts/_partials/func/ShowDate.html
New file
@@ -0,0 +1,31 @@
{{/*
    ShowDate
    Decides whether a page's date should be rendered.
    Dates are shown by default. They are hidden when the site sets
    `ananke.pages.show_date = false`, unless a page opts back in with
    `ananke.show_date = true` in its front matter. A page can also hide its
    own date with `ananke.show_date = false`. Pages without a date
    (`.Date.IsZero`) never render one.
    This mirrors the date logic used in `single.html` so that single pages and
    summary cards stay consistent.
    @param . The page context.
    @return boolean True when the date should be displayed.
*/}}
{{ return and
    (not .Date.IsZero)
    (or
        (and
            (not (eq false site.Params.ananke.pages.show_date))
            (not (eq false .Params.ananke.show_date))
        )
        (and
            (eq false site.Params.ananke.pages.show_date)
            (eq true .Params.ananke.show_date)
        )
    )
}}
layouts/list.html
@@ -1,4 +1,9 @@
{{ define "main" }}
  {{ $summary_template := "summary" }}
  {{ if $.Param "ananke.pages.show_list_images" }}
    {{ $summary_template = "summary-with-image" }}
  {{ end }}
  <article class="pa3 pa4-ns nested-copy-line-height">
    <section class="cf ph3 ph5-l pv3 pv4-l f4 tc-l center measure-wide lh-copy nested-links {{ $.Param "text_color" | compare.Default "mid-gray" }}">
      {{- .Content -}}
@@ -6,7 +11,7 @@
    <section class="flex-ns mt5 flex-wrap justify-around">
      {{ range .Paginator.Pages }}
        <div class="w-100 w-30-l mb4 relative bg-white">
          {{ .Render "summary" }}
          {{ .Render $summary_template }}
        </div>
      {{ end }}
    </section>{{/* former internal template */}}
layouts/post/list.html
@@ -2,6 +2,11 @@
{{/*
  This template is the same as the default and is here to demonstrate that if you have a content directory called "post" you can create a layouts directory, just for that section.
   */}}
  {{ $summary_template := "summary" }}
  {{ if $.Param "ananke.pages.show_list_images" }}
    {{ $summary_template = "summary-with-image" }}
  {{ end }}
  <article class="pa3 pa4-ns nested-copy-line-height">
    <section class="cf ph3 ph5-l pv3 pv4-l f4 tc-l center measure-wide lh-copy nested-links {{ $.Param "text_color" | compare.Default "mid-gray" }}">
      {{ .Content }}
@@ -9,7 +14,7 @@
    <aside class="flex-ns mt5 flex-wrap justify-around">
      {{ range .Paginator.Pages }}
        <div class="w-100 w-30-l mb4 relative bg-white">
          {{ .Render "summary" }}
          {{ .Render $summary_template }}
        </div>
      {{ end }}
    </aside>
layouts/post/summary.html
@@ -1,8 +1,9 @@
  {{ $show_date := partials.Include "func/ShowDate.html" . }}
  <div class="mb3 pa4 {{ $.Param "text_color" | compare.Default "mid-gray" }} overflow-hidden">
    {{ if .Date }}
      <div class="f6">
        {{ .Date | time.Format (compare.Default "January 2, 2006" .Site.Params.date_format) }}
      </div>
    {{ if $show_date }}
      <time class="f6 db" {{ fmt.Printf `datetime="%s"` (.Date.Format "2006-01-02T15:04:05Z07:00") | safe.HTMLAttr }}>
        {{- .Date | time.Format (compare.Default "January 2, 2006" .Site.Params.date_format) -}}
      </time>
    {{ end }}
    <h1 class="f3 near-black">
      <a href="{{ .RelPermalink }}" class="link black dim">
layouts/single.html
@@ -36,19 +36,7 @@
      </p>
      {{ end }}
      {{/* Hugo uses Go's date formatting is set by example. Here are two formats */}}
      {{ if and
          (not .Date.IsZero)
          (or
              (and
                  (not (eq false site.Params.ananke.pages.show_date))
                  (not (eq false .Params.ananke.show_date))
              )
              (and
                  (eq false site.Params.ananke.pages.show_date)
                  (eq true .Params.ananke.show_date)
              )
          )
      }}
      {{ if partials.Include "func/ShowDate.html" . }}
      <time class="f6 mv4 dib tracked" {{ fmt.Printf `datetime="%s"` (.Date.Format "2006-01-02T15:04:05Z07:00") | safe.HTMLAttr }}>
        {{- .Date | time.Format (compare.Default "January 2, 2006" .Site.Params.date_format) -}}
      </time>
layouts/summary-with-image.html
@@ -1,4 +1,5 @@
{{ $featured_image := partials.Include "func/GetFeaturedImage.html" . }}
{{ $show_date := partials.Include "func/ShowDate.html" . }}
<article class="bb b--black-10">
  <div class="db pv4 ph3 ph0-l dark-gray no-underline">
    <div class="flex-column flex-row-ns flex">
@@ -11,6 +12,11 @@
        </div>
      {{ end }}
      <div class="blah w-100{{ if $featured_image }} w-60-ns {{ compare.Conditional (compare.Eq $.Site.Language.Direction "rtl") "pr3-ns" "pl3-ns" }}{{ end }}">
        {{ if $show_date }}
          <time class="f6 db" {{ fmt.Printf `datetime="%s"` (.Date.Format "2006-01-02T15:04:05Z07:00") | safe.HTMLAttr }}>
            {{- .Date | time.Format (compare.Default "January 2, 2006" .Site.Params.date_format) -}}
          </time>
        {{ end }}
        <h1 class="f3 fw1 athelas mt0 lh-title">
          <a href="{{.RelPermalink}}" class="color-inherit dim link">
            {{ .Title }}
layouts/summary.html
@@ -1,6 +1,12 @@
{{ $show_date := partials.Include "func/ShowDate.html" . }}
<div class="w-100 mb4 nested-copy-line-height relative bg-white">
  <div class="mb3 pa4 gray overflow-hidden bg-white">
    {{with .CurrentSection.Title }}<span class="f6 db">{{ . }}</span>{{end}}
    {{ if $show_date }}
      <time class="f6 db" {{ fmt.Printf `datetime="%s"` (.Date.Format "2006-01-02T15:04:05Z07:00") | safe.HTMLAttr }}>
        {{- .Date | time.Format (compare.Default "January 2, 2006" .Site.Params.date_format) -}}
      </time>
    {{ end }}
    <h1 class="f3 near-black">
      <a href="{{ .RelPermalink }}" class="link black dim">
        {{ .Title }}
scripts/test-hugo-quickstart.ts
@@ -809,6 +809,109 @@
}
/**
 * Markup only emitted by `summary-with-image.html` when a featured image is
 * rendered, used to detect that list cards switched to the image template.
 */
const LIST_CARD_IMAGE_MARKER = 'class="img"';
/**
 * Markup emitted by the summary templates when a card date is rendered.
 */
const SUMMARY_CARD_DATE_MARKER = "datetime=";
/**
 * Create a content section whose list page exercises both the image and the
 * date behaviour of summary cards: one page has a featured image, the other
 * does not, and both carry an explicit date.
 *
 * @param contentDir Absolute path to the project `content` directory.
 */
async function writeListCardFixtures(contentDir: string): Promise<void> {
    const sectionDir = join(contentDir, "cards");
    await mkdir(sectionDir, { recursive: true });
    const sectionIndex = ["+++", "title = 'Cards'", "+++", "", ""].join("\n");
    const withImage = [
        "+++",
        "title = 'Card With Image'",
        "date = 2024-01-15T00:00:00Z",
        "featured_image = '/images/card-hero.jpg'",
        "+++",
        "",
        "Card body.",
        "",
    ].join("\n");
    const withoutImage = [
        "+++",
        "title = 'Card Without Image'",
        "date = 2024-02-20T00:00:00Z",
        "+++",
        "",
        "Card body.",
        "",
    ].join("\n");
    await writeTextFile(join(sectionDir, "_index.md"), sectionIndex);
    await writeTextFile(join(sectionDir, "with-image.md"), withImage);
    await writeTextFile(join(sectionDir, "without-image.md"), withoutImage);
}
/**
 * Assert that a list page renders summary cards with the expected image and
 * date behaviour.
 *
 * @param listHtml HTML from the rendered list page.
 * @param label Human-readable description of the configuration under test.
 * @param expectations Whether image cards and dates are expected in the output.
 * @throws Error when the rendered cards do not match the expectations.
 */
function assertListCardSummaries(
    listHtml: string,
    label: string,
    expectations: { images: boolean; dates: boolean },
): void {
    const failures: string[] = [];
    const hasImage = listHtml.includes(LIST_CARD_IMAGE_MARKER);
    const hasDate = listHtml.includes(SUMMARY_CARD_DATE_MARKER);
    if (expectations.images && !hasImage) {
        failures.push(
            "- expected image summary cards but the list rendered no card image",
        );
    }
    if (!expectations.images && hasImage) {
        failures.push(
            "- image summary cards were rendered when 'ananke.pages.show_list_images' was not enabled",
        );
    }
    if (expectations.dates && !hasDate) {
        failures.push(
            "- expected summary card dates but none were rendered by default",
        );
    }
    if (!expectations.dates && hasDate) {
        failures.push(
            "- summary card dates were rendered when 'ananke.pages.show_date' was false",
        );
    }
    if (failures.length > 0) {
        throw new Error(
            [
                `Strict assertion failed: list card summaries (${label}) 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.
@@ -1236,6 +1339,77 @@
        await assertHeaderSectionClassConfigurable(projectRoot);
        console.log("[OK ] Configurable hero header section class (issue #504)");
        console.log("\n[RUN] List page image cards and summary dates (issue #217)");
        await writeListCardFixtures(join(projectRoot, "content"));
        const cardsListPath = join(
            projectRoot,
            "public",
            "cards",
            "index.html",
        );
        // Default configuration: no image cards, but dates show by default.
        await writeTextFile(
            configPath,
            [
                "baseURL = 'https://example.com/'",
                "title = 'Ananke Test Quickstart'",
                `theme = '${options.themeName}'`,
                "",
            ].join("\n"),
        );
        const cardsDefaultBuildStep: StepDefinition = {
            name: "Build site with default summary cards",
            command: "hugo",
            args: [],
            cwd: projectRoot,
            expectedFiles: ["public/cards/index.html"],
        };
        const cardsDefaultBuildReport = await executeHugoBuildStep(
            cardsDefaultBuildStep,
            projectRoot,
        );
        reports.push(cardsDefaultBuildReport);
        assertListCardSummaries(
            await readTextFile(cardsListPath),
            "defaults",
            { images: false, dates: true },
        );
        // Opt in to image cards and disable summary dates: images appear and the
        // dates disappear, proving the two settings are independent.
        await writeTextFile(
            configPath,
            [
                "baseURL = 'https://example.com/'",
                "title = 'Ananke Test Quickstart'",
                `theme = '${options.themeName}'`,
                "[params.ananke.pages]",
                "show_list_images = true",
                "show_date = false",
                "",
            ].join("\n"),
        );
        const cardsImagesBuildStep: StepDefinition = {
            name: "Build site with image cards and dates disabled",
            command: "hugo",
            args: [],
            cwd: projectRoot,
            expectedFiles: ["public/cards/index.html"],
        };
        const cardsImagesBuildReport = await executeHugoBuildStep(
            cardsImagesBuildStep,
            projectRoot,
        );
        reports.push(cardsImagesBuildReport);
        assertListCardSummaries(
            await readTextFile(cardsListPath),
            "image cards with dates disabled",
            { images: true, dates: false },
        );
        console.log("[OK ] List page image cards and summary dates (issue #217)");
        console.log("\nResult: PASS");
        if (options.keepOnSuccess) {