]> danix's work - danix.xyz-2.git/commitdiff
feat: Add reusable tag cloud partial with A11y and dark/light mode support
authorDanilo M. <redacted>
Tue, 21 Apr 2026 20:56:01 +0000 (22:56 +0200)
committerDanilo M. <redacted>
Tue, 21 Apr 2026 20:56:01 +0000 (22:56 +0200)
- Create tag-cloud.html partial with flexible dict interface:
  * showCount (bool): Toggle count badges
  * wrapInWidget (bool): Sidebar widget wrapper with .sidebar-widget class
  * maxTags (int): Limit shown tags (used for sidebar: 15 max)
  * headingLevel (h2|h3): Configurable heading element

- Implement visual tier scaling by frequency (3 tiers):
  * low: 0.75rem, 0.75 opacity — uncommon tags
  * medium: 0.875rem, 0.88 opacity — moderate frequency
  * high: 1rem, 1 opacity, accent border — popular tags

- Add .tag-cloud and .tag-tier-* CSS classes (main.css):
  * Uses CSS variables (--accent, --border, --text-dim) for dark/light compatibility
  * Focus ring matches site standard (outline-offset: 2px)
  * Hover state: accent border + subtle bg tint
  * prefers-reduced-motion: transitions disabled

- Integrate in 3 locations:
  * Homepage (layouts/index.html): Full cloud with counts
  * Article sidebar (layouts/partials/sidebar.html): Compact widget, 15 max, no counts
  * 404 pages (404.en.html, 404.it.html): Full cloud between recent articles and nav

- A11y implementation:
  * <section aria-labelledby> landmark (non-sidebar mode)
  * <nav aria-label="Browse by topic"> named navigation
  * Each link aria-label includes count text even when visual badge hidden
  * <span aria-hidden="true"> on count badge to avoid duplication
  * Proper heading hierarchy (h2 homepage, h3 on 404)

- Add i18n keys (en.yaml, it.yaml):
  * tagCloud: "Explore Topics" / "Esplora gli argomenti"
  * exploreTopics: "Browse by topic" / "Sfoglia per argomento"

- URL handling: Use .Page.RelPermalink from OrderedTaxonomyEntry — no manual /tags/ construction, language-aware paths work automatically

Co-Authored-By: Claude Haiku 4.5 <redacted>
i18n/en.yaml
i18n/it.yaml
themes/danix-xyz-hacker/assets/css/main.css
themes/danix-xyz-hacker/assets/css/main.min.css
themes/danix-xyz-hacker/layouts/404.en.html
themes/danix-xyz-hacker/layouts/404.it.html
themes/danix-xyz-hacker/layouts/index.html
themes/danix-xyz-hacker/layouts/partials/sidebar.html
themes/danix-xyz-hacker/layouts/partials/tag-cloud.html [new file with mode: 0644]

index 7e8b71fe65669c5ffbe3e69521ca758e1fc359a4..ea264a729f730ce3d08c8a8da9cd6ea545301fa8 100644 (file)
@@ -47,6 +47,8 @@ category: "Category"
 categories: "Categories"
 tag: "Tag"
 tags: "Tags"
+tagCloud: "Explore Topics"
+exploreTopics: "Browse by topic"
 relatedPosts: "Related articles"
 noRelated: "No related articles."
 postCount:
index 43cb604a50f00fb397b85a34561029677e8c325d..9cfa182bbd839d49f6351345414c74c53fbc5d0f 100644 (file)
@@ -47,6 +47,8 @@ category: "Categoria"
 categories: "Categorie"
 tag: "Tag"
 tags: "Tag"
+tagCloud: "Esplora gli argomenti"
+exploreTopics: "Sfoglia per argomento"
 relatedPosts: "Articoli correlati"
 noRelated: "Nessun articolo correlato."
 postCount:
index 1588b6162c711274b8854f93e287c507c339a045..31a977d109885d85712cd8e2eecd28bdda3feddf 100644 (file)
@@ -350,6 +350,82 @@ html.theme-light picture img[src="/images/default_thumbnail_dark.png"] {
     margin-bottom: 1.5rem;
   }
 
+  /* =====================
+     Tag Cloud Component
+     ===================== */
+
+  .tag-cloud {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 0.5rem;
+    align-items: baseline;
+  }
+
+  .tag-cloud-link {
+    display: inline-flex;
+    align-items: center;
+    gap: 0.375rem;
+    padding: 0.25rem 0.625rem;
+    border: 1px solid var(--border);
+    border-radius: 0.25rem;
+    font-family: var(--font-mono, 'JetBrains Mono', monospace);
+    font-size: 0.75rem;
+    color: var(--text-dim);
+    text-decoration: none;
+    transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out;
+    white-space: nowrap;
+    line-height: 1.4;
+  }
+
+  .tag-cloud-link:hover {
+    border-color: rgba(var(--accent-rgb), 0.5);
+    color: var(--accent);
+    background-color: rgba(var(--accent-rgb), 0.06);
+  }
+
+  .tag-cloud-link:focus-visible {
+    outline: 2px solid var(--accent);
+    outline-offset: 2px;
+    border-radius: 0.25rem;
+  }
+
+  .tag-tier-low {
+    font-size: 0.75rem;
+    opacity: 0.75;
+  }
+
+  .tag-tier-medium {
+    font-size: 0.875rem;
+    opacity: 0.88;
+  }
+
+  .tag-tier-high {
+    font-size: 1rem;
+    opacity: 1;
+    border-color: rgba(var(--accent-rgb), 0.3);
+    color: var(--text);
+  }
+
+  .tag-cloud-count {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    padding: 0 0.375rem;
+    border-radius: 9999px;
+    font-size: 0.65em;
+    font-weight: 600;
+    background-color: rgba(var(--accent-rgb), 0.12);
+    color: var(--accent);
+    line-height: 1.6;
+    min-width: 1.2em;
+  }
+
+  @media (prefers-reduced-motion: reduce) {
+    .tag-cloud-link {
+      transition: none;
+    }
+  }
+
   .share-grid {
     display: grid;
     grid-template-columns: repeat(3, 50px);
index 86043923a50d633819d2f7e633feda5fdf7cab2f..236b11c7a57913192915d4773fcafe814a3a241c 100644 (file)
@@ -1448,6 +1448,65 @@ button,
   margin-bottom: 1.5rem;
 }
 
+/* =====================
+     Tag Cloud Component
+     ===================== */
+
+.tag-cloud {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 0.5rem;
+  align-items: baseline;
+}
+
+.tag-cloud-link {
+  display: inline-flex;
+  align-items: center;
+  gap: 0.375rem;
+  padding: 0.25rem 0.625rem;
+  border: 1px solid var(--border);
+  border-radius: 0.25rem;
+  font-family: var(--font-mono, 'JetBrains Mono', monospace);
+  font-size: 0.75rem;
+  color: var(--text-dim);
+  text-decoration: none;
+  transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out;
+  white-space: nowrap;
+  line-height: 1.4;
+}
+
+.tag-cloud-link:hover {
+  border-color: rgba(var(--accent-rgb), 0.5);
+  color: var(--accent);
+  background-color: rgba(var(--accent-rgb), 0.06);
+}
+
+.tag-cloud-link:focus-visible {
+  outline: 2px solid var(--accent);
+  outline-offset: 2px;
+  border-radius: 0.25rem;
+}
+
+.tag-cloud-count {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 0.375rem;
+  border-radius: 9999px;
+  font-size: 0.65em;
+  font-weight: 600;
+  background-color: rgba(var(--accent-rgb), 0.12);
+  color: var(--accent);
+  line-height: 1.6;
+  min-width: 1.2em;
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .tag-cloud-link {
+    transition: none;
+  }
+}
+
 .share-grid {
   display: grid;
   grid-template-columns: repeat(3, 50px);
index 8d8641db994248bc38474d35a095618da5e25f1b..2829a20cf0c1e3e352f3ad7904febecbc6cd359a 100644 (file)
       </div>
     </div>
 
+    <!-- Explore Topics -->
+    <div class="mb-12 text-left">
+      {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }}
+    </div>
+
     <!-- Navigation Links -->
     <div class="space-y-4 flex flex-col items-center mb-12">
       <a href="/" class="btn btn-primary">
index 1a8fbbe3bb54c1f457551ce63e363cb3c1e70237..d876371ae0dc0cf1c7f21a68c19722ef8e24563a 100644 (file)
       </div>
     </div>
 
+    <!-- Explore Topics -->
+    <div class="mb-12 text-left">
+      {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }}
+    </div>
+
     <!-- Navigation Links -->
     <div class="space-y-4 flex flex-col items-center mb-12">
       <a href="/it/" class="btn btn-primary">
index 7247c3f0666a4408f13105ac6bdfa750dc6e2e76..a290d6aa33469fb73bf911a425b03f65977c9858 100644 (file)
         {{ i18n "contact" }}
       </a>
     </div>
+
+    <!-- Tag Cloud Section -->
+    <div class="mt-16 pt-8 border-t border-border">
+      {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h2") }}
+    </div>
   </div>
 </section>
 {{ end }}
index dc263e6a18cfcc878325be46b0baedfbf9186f3f..a2225f1be59d224c68bd8b5985ea282a7221b063 100644 (file)
@@ -46,4 +46,9 @@
     {{ end }}
   </div>
   {{ end }}
+
+  <hr class="sidebar-hr">
+
+  <!-- Tag Cloud Widget -->
+  {{ partial "tag-cloud.html" (dict "page" . "showCount" false "wrapInWidget" true "maxTags" 15) }}
 </aside>
diff --git a/themes/danix-xyz-hacker/layouts/partials/tag-cloud.html b/themes/danix-xyz-hacker/layouts/partials/tag-cloud.html
new file mode 100644 (file)
index 0000000..0d59e3c
--- /dev/null
@@ -0,0 +1,85 @@
+{{/* tag-cloud.html
+  Reusable tag cloud partial for homepage, sidebar, and 404 pages.
+
+  Params (dict):
+    page         Page    required — calling page context (provides .Site.Taxonomies.tags, .Lang)
+    showCount    bool    optional — show post count per tag (default true)
+    heading      string  optional — heading text override (default: i18n "tagCloud")
+    headingLevel string  optional — h2|h3|p for non-widget mode (default "h2")
+    wrapInWidget bool    optional — wrap in .sidebar-widget for sidebar placement (default false)
+    maxTags      int     optional — max tags to show, 0 = all (default 0)
+*/}}
+
+{{- $page         := .page -}}
+{{- $showCount    := .showCount    | default true -}}
+{{- $heading      := .heading      | default (i18n "tagCloud") -}}
+{{- $headingLevel := .headingLevel | default "h2" -}}
+{{- $wrapInWidget := .wrapInWidget | default false -}}
+{{- $maxTags      := .maxTags      | default 0 -}}
+
+{{- $tags := $page.Site.Taxonomies.tags -}}
+
+{{/* Early exit if no tags */}}
+{{- if $tags -}}
+
+{{/* Compute max count for tier thresholds */}}
+{{- $maxCount := 0 -}}
+{{- range $tags -}}
+  {{- if gt .Count $maxCount -}}{{- $maxCount = .Count -}}{{- end -}}
+{{- end -}}
+
+{{/* Tier thresholds (integer division) */}}
+{{- $tierMedThreshold := div $maxCount 3 -}}
+{{- $tierHighThreshold := mul (div $maxCount 3) 2 -}}
+
+{{/* Ordered tag list (descending by count) */}}
+{{- $orderedTags := $tags.ByCount -}}
+{{- if gt $maxTags 0 -}}
+  {{- $orderedTags = first $maxTags $orderedTags -}}
+{{- end -}}
+
+{{/* Render based on placement mode */}}
+{{- if $wrapInWidget -}}
+<div class="sidebar-widget">
+  <p class="sidebar-widget-label"># {{ i18n "tags" }}</p>
+  <nav aria-label="{{ i18n "exploreTopics" }}">
+    <div class="tag-cloud">
+{{- else -}}
+<section aria-labelledby="tag-cloud-heading">
+  <{{ $headingLevel }} id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4">
+    {{ $heading }}
+  </{{ $headingLevel }}>
+  <nav aria-label="{{ i18n "exploreTopics" }}">
+    <div class="tag-cloud">
+{{- end -}}
+
+      {{- range $orderedTags -}}
+        {{- $count := .Count -}}
+        {{- $tier := "low" -}}
+        {{- if gt $count $tierHighThreshold -}}
+          {{- $tier = "high" -}}
+        {{- else if gt $count $tierMedThreshold -}}
+          {{- $tier = "medium" -}}
+        {{- end -}}
+        <a
+          href="{{ .Page.RelPermalink }}"
+          class="tag-cloud-link tag-tier-{{ $tier }}"
+          aria-label="{{ .Name }}{{- if $showCount }} ({{ i18n "postCount" $count }}){{- end -}}"
+        >
+          {{- .Name -}}
+          {{- if $showCount -}}
+          <span class="tag-cloud-count" aria-hidden="true">{{ $count }}</span>
+          {{- end -}}
+        </a>
+      {{- end -}}
+
+    </div>
+  </nav>
+
+{{- if $wrapInWidget -}}
+</div>
+{{- else -}}
+</section>
+{{- end -}}
+
+{{- end -}}{{/* end if $tags */}}