From 88863335fd1e4f387415e68115828e8d75cb0bcc Mon Sep 17 00:00:00 2001 From: Kevin Horst Date: Fri, 8 May 2026 19:20:53 +0200 Subject: [PATCH 1/2] feat: add ansible galaxy card shortcode --- assets/icons/ansible.svg | 2 + assets/js/fetch-repo.js | 25 ++- exampleSite/content/docs/shortcodes/index.md | 31 ++++ layouts/shortcodes/ansible.html | 174 +++++++++++++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 assets/icons/ansible.svg create mode 100644 layouts/shortcodes/ansible.html diff --git a/assets/icons/ansible.svg b/assets/icons/ansible.svg new file mode 100644 index 00000000..afc3718d --- /dev/null +++ b/assets/icons/ansible.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/js/fetch-repo.js b/assets/js/fetch-repo.js index 61bedfd0..0ea2bf67 100644 --- a/assets/js/fetch-repo.js +++ b/assets/js/fetch-repo.js @@ -12,6 +12,14 @@ } const platforms = { + "ansible-role": { + "results.0.download_count": "downloads", + "results.0.summary_fields.versions.0.name": "version", + }, + "ansible-collection": { + download_count: "downloads", + "highest_version.version": "version", + }, github: { full_name: "full_name", description: "description", @@ -49,12 +57,27 @@ }, }; + const formatThousands = (value) => + value == null ? value : Number(value).toLocaleString("en-US"); + const processors = { huggingface: { description: (value) => value?.replace(/Dataset Card for .+?\s+Dataset Summary\s+/, "").trim() || value, }, + "ansible-role": { + "results.0.download_count": formatThousands, + }, + "ansible-collection": { + download_count: formatThousands, + }, }; + const getNested = (obj, path) => + path.split(".").reduce((acc, key) => { + if (acc == null) return undefined; + return Array.isArray(acc) ? acc[Number(key)] : acc[key]; + }, obj); + const platform = Object.keys(platforms).find((p) => repoId.startsWith(p)) || "github"; const mapping = platforms[platform]; @@ -77,7 +100,7 @@ Object.entries(mapping).forEach(([dataField, elementSuffix]) => { const element = document.getElementById(`${repoId}-${elementSuffix}`); if (element) { - let value = data[dataField]; + let value = getNested(data, dataField); if (processors[platform]?.[dataField]) { value = processors[platform][dataField](value); } diff --git a/exampleSite/content/docs/shortcodes/index.md b/exampleSite/content/docs/shortcodes/index.md index 707deb26..af0a381e 100644 --- a/exampleSite/content/docs/shortcodes/index.md +++ b/exampleSite/content/docs/shortcodes/index.md @@ -187,6 +187,37 @@ The alert sign (`+` or `-`) is optional to control whether the admonition is fol


+## Ansible Galaxy Card + +`ansible` renders a card for an [Ansible Galaxy](https://galaxy.ansible.com/) entry, fetched at build time. It accepts either a `role` or a `collection` parameter, both in `namespace.name` form. + + +| Parameter | Description | +| ------------ | ------------------------------------------------------------------------------------ | +| `role` | [String] Galaxy role in the format `namespace.name`, e.g. `geerlingguy.docker` | +| `collection` | [String] Galaxy collection in the format `namespace.name`, e.g. `community.general` | + + +Set exactly one of `role` or `collection` per call. + +**Example 1: Role** + +```md +{{}} +``` + +{{< ansible role="geerlingguy.docker" >}} + +**Example 2: Collection** + +```md +{{}} +``` + +{{< ansible collection="community.general" >}} + +


+ ## Article `Article` will embed a single article into a markdown file. The `link` to the file should be the `.RelPermalink` of the file to be embedded. Note that the shortcode will not display anything if it's referencing it's parent. _Note: if you are running your website in a subfolder like Blowfish (i.e. /blowfish/) please include that path in the link._ diff --git a/layouts/shortcodes/ansible.html b/layouts/shortcodes/ansible.html new file mode 100644 index 00000000..99c5306a --- /dev/null +++ b/layouts/shortcodes/ansible.html @@ -0,0 +1,174 @@ +{{- $role := .Get "role" -}} +{{- $collection := .Get "collection" -}} +{{- $apiURL := "" -}} +{{- $repoLink := "" -}} +{{- $type := "" -}} +{{- $namespace := "" -}} +{{- $name := "" -}} + +{{- if $role -}} + {{- $parts := split $role "." -}} + {{- $namespace = index $parts 0 -}} + {{- $name = index $parts 1 -}} + {{- $apiURL = print "https://galaxy.ansible.com/api/v1/roles/?owner__username=" $namespace "&name=" $name -}} + {{- $repoLink = print "https://galaxy.ansible.com/ui/standalone/roles/" $namespace "/" $name "/" -}} + {{- $type = "role" -}} +{{- else if $collection -}} + {{- $parts := split $collection "." -}} + {{- $namespace = index $parts 0 -}} + {{- $name = index $parts 1 -}} + {{- $apiURL = print "https://galaxy.ansible.com/api/v3/plugin/ansible/content/published/collections/index/" $namespace "/" $name "/" -}} + {{- $repoLink = print "https://galaxy.ansible.com/ui/repo/published/" $namespace "/" $name "/" -}} + {{- $type = "collection" -}} +{{- end -}} + +{{ $id := delimit (slice "ansible" $type (partial "functions/uid.html" .)) "-" }} + +{{- $title := "" -}} +{{- $description := "" -}} +{{- $downloads := 0 -}} +{{- $version := "" -}} +{{- $tags := slice -}} +{{- $license := "" -}} +{{- $dataAvailable := false -}} + +{{- /* Formats an integer with comma thousand separators (lang.NumFmt is unavailable in this template context). */ -}} +{{- define "_format-int" -}} + {{- $n := printf "%d" (int .) -}} + {{- $out := "" -}} + {{- $len := len $n -}} + {{- range $i := seq $len -}} + {{- $idx := sub $i 1 -}} + {{- $c := substr $n $idx 1 -}} + {{- if and (gt $idx 0) (eq (mod (sub $len $idx) 3) 0) -}} + {{- $out = print $out "," $c -}} + {{- else -}} + {{- $out = print $out $c -}} + {{- end -}} + {{- end -}} + {{- $out -}} +{{- end -}} + +{{- with try (resources.GetRemote $apiURL) -}} + {{- with .Err -}} + {{- warnf "ansible shortcode: failed to fetch remote resource from %q: %s" $apiURL $.Position -}} + {{- else with .Value -}} + {{- $resp := . | transform.Unmarshal -}} + {{- if eq $type "role" -}} + {{- with index ($resp.results | default slice) 0 -}} + {{- $title = print $namespace "." .name -}} + {{- $description = .description -}} + {{- $downloads = .download_count -}} + {{- with .summary_fields -}} + {{- with index (.versions | default slice) 0 -}} + {{- $version = .name -}} + {{- end -}} + {{- $tags = .tags -}} + {{- end -}} + {{- $dataAvailable = true -}} + {{- end -}} + {{- else if eq $type "collection" -}} + {{- $title = print $namespace "." $name -}} + {{- $downloads = $resp.download_count -}} + {{- with $resp.highest_version -}} + {{- $version = .version -}} + {{- end -}} + {{- if $version -}} + {{- $versionURL := print $apiURL "versions/" $version "/" -}} + {{- with try (resources.GetRemote $versionURL) -}} + {{- with .Value -}} + {{- $verResp := . | transform.Unmarshal -}} + {{- with $verResp.metadata -}} + {{- $description = .description -}} + {{- $tags = .tags -}} + {{- with .license -}} + {{- $license = index . 0 -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- $dataAvailable = true -}} + {{- end -}} + {{- else -}} + {{- warnf "ansible shortcode: unable to get remote resource from %q: %s" $apiURL $.Position -}} + {{- end -}} +{{- end -}} + +{{- if $dataAvailable -}} +
+ +
+
+ + {{ partial "icon.html" "ansible" }} + +
+ {{ $title }} +
+
+ + {{- if $description }} +

+ {{ $description | markdownify }} +

+ {{- end }} + +
+ + {{ $type }} + + + + {{ partial "icon.html" "download" }} + +
+ {{ template "_format-int" $downloads }} +
+ + {{- if $version }} + + {{ partial "icon.html" "tag" }} + +
+ {{ $version }} +
+ {{- end }} + + {{- if $license }} + + {{ partial "icon.html" "scale-balanced" }} + +
+ {{ $license }} +
+ {{- end }} +
+ + {{- if $tags }} +
+ {{- range first 5 $tags }} + + {{ . }} + + {{- end }} +
+ {{- end }} +
+ {{ $fetchRepo := resources.Get "js/fetch-repo.js" }} + {{ $fetchRepo = $fetchRepo | resources.Minify | resources.Fingerprint ($.Site.Params.fingerprintAlgorithm | default "sha512") }} + +
+
+{{- else if $apiURL -}} + {{ warnf "ansible shortcode: unable to fetch %q: %s" $apiURL .Position }} +{{- end -}} From a9869a55a6360891a6bd704cb252c9452f4123d1 Mon Sep 17 00:00:00 2001 From: Kevin Horst Date: Fri, 8 May 2026 19:26:55 +0200 Subject: [PATCH 2/2] fix: ansible card values are build-time only (Galaxy blocks CORS) --- assets/js/fetch-repo.js | 25 +------------------- exampleSite/content/docs/shortcodes/index.md | 2 ++ layouts/shortcodes/ansible.html | 15 +++--------- 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/assets/js/fetch-repo.js b/assets/js/fetch-repo.js index 0ea2bf67..61bedfd0 100644 --- a/assets/js/fetch-repo.js +++ b/assets/js/fetch-repo.js @@ -12,14 +12,6 @@ } const platforms = { - "ansible-role": { - "results.0.download_count": "downloads", - "results.0.summary_fields.versions.0.name": "version", - }, - "ansible-collection": { - download_count: "downloads", - "highest_version.version": "version", - }, github: { full_name: "full_name", description: "description", @@ -57,27 +49,12 @@ }, }; - const formatThousands = (value) => - value == null ? value : Number(value).toLocaleString("en-US"); - const processors = { huggingface: { description: (value) => value?.replace(/Dataset Card for .+?\s+Dataset Summary\s+/, "").trim() || value, }, - "ansible-role": { - "results.0.download_count": formatThousands, - }, - "ansible-collection": { - download_count: formatThousands, - }, }; - const getNested = (obj, path) => - path.split(".").reduce((acc, key) => { - if (acc == null) return undefined; - return Array.isArray(acc) ? acc[Number(key)] : acc[key]; - }, obj); - const platform = Object.keys(platforms).find((p) => repoId.startsWith(p)) || "github"; const mapping = platforms[platform]; @@ -100,7 +77,7 @@ Object.entries(mapping).forEach(([dataField, elementSuffix]) => { const element = document.getElementById(`${repoId}-${elementSuffix}`); if (element) { - let value = getNested(data, dataField); + let value = data[dataField]; if (processors[platform]?.[dataField]) { value = processors[platform][dataField](value); } diff --git a/exampleSite/content/docs/shortcodes/index.md b/exampleSite/content/docs/shortcodes/index.md index af0a381e..f97cdd7f 100644 --- a/exampleSite/content/docs/shortcodes/index.md +++ b/exampleSite/content/docs/shortcodes/index.md @@ -200,6 +200,8 @@ The alert sign (`+` or `-`) is optional to control whether the admonition is fol Set exactly one of `role` or `collection` per call. +All card values are fetched at build time via Hugo's `resources.GetRemote`. Galaxy does not allow cross-origin requests, so the card is not refreshed in the browser — rebuild the site to update the values. + **Example 1: Role** ```md diff --git a/layouts/shortcodes/ansible.html b/layouts/shortcodes/ansible.html index 99c5306a..2fbf042d 100644 --- a/layouts/shortcodes/ansible.html +++ b/layouts/shortcodes/ansible.html @@ -22,7 +22,7 @@ {{- $type = "collection" -}} {{- end -}} -{{ $id := delimit (slice "ansible" $type (partial "functions/uid.html" .)) "-" }} +{{ $id := delimit (slice "ansible" (partial "functions/uid.html" .)) "-" }} {{- $title := "" -}} {{- $description := "" -}} @@ -124,7 +124,7 @@ {{ partial "icon.html" "download" }} -
+
{{ template "_format-int" $downloads }}
@@ -132,7 +132,7 @@ {{ partial "icon.html" "tag" }} -
+
{{ $version }}
{{- end }} @@ -158,15 +158,6 @@
{{- end }}
- {{ $fetchRepo := resources.Get "js/fetch-repo.js" }} - {{ $fetchRepo = $fetchRepo | resources.Minify | resources.Fingerprint ($.Site.Params.fingerprintAlgorithm | default "sha512") }} - {{- else if $apiURL -}}