Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ An experimental theme for the [Hugo](https://gohugo.io/) static site generator.

```
articles-filter articles-list figure files-list
gallery github-content include marginalia
forge-content gallery include marginalia
menu-social model-viewer section simple-signup
dailymotion
```
Expand Down Expand Up @@ -184,6 +184,33 @@ Combine by URL-encoding a space, e.g. `![diagram](architecture.svg#blend%20shado
The `{{< include >}}` shortcode inlines content as-is and does NOT process `#fragment` modifiers — use `{{< figure >}}` or plain markdown image syntax instead.


## Forge content

`{{< forge-content >}}` fetches a README (or any file) from a public GitHub / GitLab / Forgejo / Codeberg repo at build time and inlines its rendered Markdown. It supersedes the legacy `github-content` shortcode (removed in this release); migrate by changing the shortcode name and prefixing the host to `repository`.

```hugo
{{< forge-content repository="github.com/owner/repo" branch="main" >}}
{{< forge-content repository="gitlab.com/group/project" branch="main" >}}
{{< forge-content repository="codeberg.org/owner/repo" branch="main" >}}
```

| Parameter | Default | Effect |
| ------------ | ------------ | ---------------------------------------------------------------------- |
| `repository` | — | `host/owner/repo` (unified) or `owner/repo` (legacy, GitHub assumed). |
| `branch` | `master` | Branch / tag / ref. |
| `path` | (empty) | File path. Empty fetches the README — GitHub uses its dedicated endpoint; GitLab / Forgejo probe `README.md`, `README`, `readme.md` in order. |
| `platform` | auto-detect | `github` / `gitlab` / `forgejo`. Required for unrecognised hosts (e.g. self-hosted GitLab — also addable to `params.forgeContent.gitlabHosts`). |
| `unsafe` | `false` | When `true`, allows inline `<svg>` / `<math>` from trusted sources. All other dangerous tags remain stripped regardless. |

**Security.** Untrusted remote markdown passes through three filters before `markdownify`:

1. **Hugo shortcode delimiters are neutralised** — `{{<`, `{{%`, `>}}`, `%}}` are replaced with full-width lookalikes (`{{`, `}}`) so a malicious README cannot inject server-side template directives.
2. **Dangerous HTML tags are entity-escaped** — `<script>`, `<iframe>`, `<object>`, `<embed>`, `<form>`, `<input>`, `<button>`, `<style>`, `<link>`, `<meta>`, `<base>`, `<noscript>` (and `<svg>` / `<math>` unless `unsafe="true"`) render as visible escaped text rather than executing.
3. **Dangerous attributes are stripped** — `on*=` event handlers, `javascript:` URIs in `href` / `src` / `xlink:href`, and IE `expression()` styles are removed wherever they appear; the containing tag survives but loses the attack vector.

This is a denylist, not a strict GFM allowlist. For most READMEs it's sufficient; for syndicating content from attacker-controlled repositories, run a dedicated sanitiser as a build step.


## Installation

The theme works on **Hugo ≥ 0.154** (Extended is strongly recommended for WebP conversion). Pick one install path:
Expand Down
16 changes: 10 additions & 6 deletions exampleSite/content/posts/github-demo/index.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
---
title: "GitHub content demo"
date: 2026-04-18
description: "Pulls a README from a public GitHub repo at build time."
title: "Forge content demo"
date: 2026-04-29
description: "Inlines a README from a public GitHub repo at build time via the multi-platform forge-content shortcode."
tags: ["shortcodes", "theme"]
---

The `github-content` shortcode fetches a repository's README via the
unauthenticated GitHub API and inlines its rendered Markdown. If the
The `forge-content` shortcode fetches a README (or any file) from a
public GitHub / GitLab / Forgejo / Codeberg repo at build time and
inlines its rendered Markdown. Untrusted remote content is filtered
through three security passes before reaching `markdownify`: Hugo
shortcode delimiters are neutralised, dangerous HTML tags are
entity-escaped, and event-handler attributes are stripped. If the
network call fails, it warns and falls back to a link.

{{< github-content repository="gohugoio/hugo-mod-bootstrap-scss" branch="main" >}}
{{< forge-content repository="github.com/gohugoio/hugo-mod-bootstrap-scss" branch="main" >}}
6 changes: 3 additions & 3 deletions i18n/el.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ forge-last-pushed:
other: Τελευταία ώθηση


## github-content shortcode (legacy)
## forge-content shortcode

github-error:
other: "Σφάλμα φόρτωσης. Προβολή αποθετηρίου στο GitHub:"
forge-error:
other: "Δεν ήταν δυνατή η ανάκτηση από"


## 404 error
Expand Down
6 changes: 3 additions & 3 deletions i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ forge-last-pushed:
other: Last pushed


## github-content shortcode (legacy)
## forge-content shortcode

github-error:
other: "Error fetching content. View repository on GitHub:"
forge-error:
other: "Could not fetch content from"


## 404 error
Expand Down
117 changes: 14 additions & 103 deletions layouts/partials/forge-meta.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,123 +140,36 @@
Override per-page with `forge_platform` (see above).

─────────────────────────────────────────────────────────────────────────────
REQUIRED HUGO VERSION: ≥ 0.154.0 (for `try`). The theme-wide floor in
`theme.toml` / `hugo.yaml` is bumped to match.
REQUIRED HUGO VERSION: ≥ 0.154.0 (for `try` statement)
─────────────────────────────────────────────────────────────────────────────

*/ -}}

{{- /* ================================================================= */ -}}
{{- /* 1. Resolve the forge value (unified `Forge` or legacy `Github`) */ -}}
{{- /* 1–4. Resolve forge / host / platform / label via shared partial */ -}}
{{- /* ================================================================= */ -}}

{{- $forgeRaw := "" -}}
{{- $isLegacyGithub := false -}}
{{- $resolved := partial "forge-resolve" (dict
"forge" .Params.Forge
"github" .Params.Github
"platform" .Params.forge_platform
"label" .Params.forge_label
"pageTitle" .Title
"component" "forge-meta") -}}

{{- with .Params.Forge -}}
{{- $forgeRaw = . -}}
{{- else -}}
{{- with $.Params.Github -}}
{{- $forgeRaw = . -}}
{{- $isLegacyGithub = true -}}
{{- end -}}
{{- end -}}

{{- with $forgeRaw -}}

{{- /* ================================================================= */ -}}
{{- /* 2. Parse host and repo path */ -}}
{{- /* ================================================================= */ -}}

{{- $host := "" -}}
{{- $repoPath := "" -}}

{{- if $isLegacyGithub -}}
{{- /* Legacy format: "owner/repo" — no host prefix */ -}}
{{- $host = "github.com" -}}
{{- $repoPath = $forgeRaw -}}
{{- else -}}
{{- /* Unified format: "host/owner/repo" or "host/group/subgroup/project" */ -}}
{{- $parts := split $forgeRaw "/" -}}
{{- if ge (len $parts) 3 -}}
{{- $host = index $parts 0 -}}
{{- $repoPath = strings.TrimPrefix (printf "%s/" $host) $forgeRaw -}}
{{- else -}}
{{- warnf "forge-meta: invalid Forge value %q on page %q — expected host/owner/repo" $forgeRaw $.Page.Title -}}
{{- end -}}
{{- end -}}

{{- with $host -}}

{{- /* ================================================================= */ -}}
{{- /* 3. Detect platform (auto or per-page override) */ -}}
{{- /* ================================================================= */ -}}

{{- $platform := "" -}}

{{- /* Per-page override takes priority */ -}}
{{- with $.Params.forge_platform -}}
{{- if in (slice "github" "gitlab" "forgejo") . -}}
{{- $platform = . -}}
{{- else -}}
{{- warnf "forge-meta: unknown forge_platform %q on page %q — expected github, gitlab, or forgejo" . $.Page.Title -}}
{{- end -}}
{{- else -}}
{{- /* Auto-detect from hostname */ -}}
{{- if eq $host "github.com" -}}
{{- $platform = "github" -}}
{{- else if eq $host "gitlab.com" -}}
{{- $platform = "gitlab" -}}
{{- else if eq $host "codeberg.org" -}}
{{- $platform = "forgejo" -}}
{{- else -}}
{{- /* Check user-configured GitLab hosts (nil-safe) */ -}}
{{- $gitlabHosts := slice -}}
{{- with site.Params.forgeContent -}}
{{- with .gitlabHosts -}}
{{- $gitlabHosts = . -}}
{{- end -}}
{{- end -}}
{{- range $gitlabHosts -}}
{{- if eq . $host -}}
{{- $platform = "gitlab" -}}
{{- end -}}
{{- end -}}
{{- /* If still unresolved, warn */ -}}
{{- if not $platform -}}
{{- warnf "forge-meta: unrecognised host %q on page %q — set forge_platform in front matter" $host $.Page.Title -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $host := $resolved.host -}}
{{- $repoPath := $resolved.repoPath -}}
{{- $platform := $resolved.platform -}}
{{- $platformLabel := $resolved.platformLabel -}}
{{- $webURL := $resolved.webURL -}}

{{- /* Only proceed if platform was resolved */ -}}
{{- if $platform -}}

{{- /* ================================================================= */ -}}
{{- /* 4. Resolve display label */ -}}
{{- /* ================================================================= */ -}}

{{- $defaultLabels := dict "github" "GitHub" "gitlab" "GitLab" "forgejo" "Forgejo" -}}
{{- $platformLabel := index $defaultLabels $platform -}}

{{- /* Codeberg gets its own default label */ -}}
{{- if and (eq $platform "forgejo") (eq $host "codeberg.org") -}}
{{- $platformLabel = "Codeberg" -}}
{{- end -}}

{{- /* Per-page override wins over everything */ -}}
{{- with $.Params.forge_label -}}
{{- $platformLabel = . -}}
{{- end -}}

{{- /* ================================================================= */ -}}
{{- /* 5. Build API URL */ -}}
{{- /* ================================================================= */ -}}

{{- /* The web URL always uses the host the user provided (supports
redirect / vanity domains). */ -}}
{{- $webURL := printf "https://%s/%s" $host $repoPath -}}

{{- $apiURL := "" -}}

{{- if eq $platform "github" -}}
Expand Down Expand Up @@ -356,5 +269,3 @@
{{- end -}}{{- /* end with $stats (render gate) */ -}}

{{- end -}}{{- /* end if $platform */ -}}
{{- end -}}{{- /* end with $host */ -}}
{{- end -}}{{- /* end with $forgeRaw */ -}}
129 changes: 129 additions & 0 deletions layouts/partials/forge-resolve.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
{{- /*
forge-resolve.html — Shared platform resolution for forge-* templates.

Used by `partials/forge-meta.html` and `shortcodes/forge-content.html`
so they can never drift on platform detection, label resolution, or
warning messages.

─── Input (dict) ──────────────────────────────────────────────────────
forge (string) — the unified "host/owner/repo" value
github (string) — legacy "owner/repo" fallback (assumed GitHub)
platform (string) — per-page override: "github" | "gitlab" | "forgejo"
label (string) — display-name override
pageTitle (string) — used in warning messages
component (string) — caller name for warnings (e.g. "forge-meta",
"forge-content"); defaults to "forge"

Either `forge` OR `github` must be non-empty for a valid result.

─── Output (dict) ─────────────────────────────────────────────────────
host (string) — e.g. "github.com"
repoPath (string) — "owner/repo" or "group/subgroup/project"
platform (string) — "github" | "gitlab" | "forgejo" | ""
platformLabel (string) — "GitHub" | "Codeberg" | overridden | ""
webURL (string) — "https://{host}/{repoPath}"
isLegacyGithub (bool) — true when input came via `github`

When `platform` in the returned dict is empty, the caller MUST
early-exit (the helper has already emitted any relevant warnf).
*/ -}}

{{- $forge := .forge -}}
{{- $github := .github -}}
{{- $platformIn := .platform -}}
{{- $labelIn := .label -}}
{{- $pageTitle := .pageTitle -}}
{{- $component := .component | default "forge" -}}

{{- $forgeRaw := "" -}}
{{- $isLegacyGithub := false -}}
{{- with $forge -}}
{{- $forgeRaw = . -}}
{{- else -}}
{{- with $github -}}
{{- $forgeRaw = . -}}
{{- $isLegacyGithub = true -}}
{{- end -}}
{{- end -}}

{{- $host := "" -}}
{{- $repoPath := "" -}}
{{- $platform := "" -}}
{{- $platformLabel := "" -}}
{{- $webURL := "" -}}

{{- with $forgeRaw -}}

{{- if $isLegacyGithub -}}
{{- $host = "github.com" -}}
{{- $repoPath = $forgeRaw -}}
{{- else -}}
{{- $parts := split $forgeRaw "/" -}}
{{- if ge (len $parts) 3 -}}
{{- $host = index $parts 0 -}}
{{- $repoPath = strings.TrimPrefix (printf "%s/" $host) $forgeRaw -}}
{{- else -}}
{{- warnf "%s: invalid forge value %q on page %q — expected host/owner/repo" $component $forgeRaw $pageTitle -}}
{{- end -}}
{{- end -}}

{{- with $host -}}

{{- /* Per-page platform override */ -}}
{{- with $platformIn -}}
{{- if in (slice "github" "gitlab" "forgejo") . -}}
{{- $platform = . -}}
{{- else -}}
{{- warnf "%s: unknown forge_platform %q on page %q — expected github, gitlab, or forgejo" $component . $pageTitle -}}
{{- end -}}
{{- else -}}
{{- /* Auto-detect from hostname */ -}}
{{- if eq $host "github.com" -}}
{{- $platform = "github" -}}
{{- else if eq $host "gitlab.com" -}}
{{- $platform = "gitlab" -}}
{{- else if eq $host "codeberg.org" -}}
{{- $platform = "forgejo" -}}
{{- else -}}
{{- $gitlabHosts := slice -}}
{{- with site.Params.forgeContent -}}
{{- with .gitlabHosts -}}
{{- $gitlabHosts = . -}}
{{- end -}}
{{- end -}}
{{- range $gitlabHosts -}}
{{- if eq . $host -}}
{{- $platform = "gitlab" -}}
{{- end -}}
{{- end -}}
{{- if not $platform -}}
{{- warnf "%s: unrecognised host %q on page %q — set forge_platform in front matter" $component $host $pageTitle -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{- /* Display label */ -}}
{{- with $platform -}}
{{- $defaultLabels := dict "github" "GitHub" "gitlab" "GitLab" "forgejo" "Forgejo" -}}
{{- $platformLabel = index $defaultLabels . -}}
{{- if and (eq . "forgejo") (eq $host "codeberg.org") -}}
{{- $platformLabel = "Codeberg" -}}
{{- end -}}
{{- with $labelIn -}}
{{- $platformLabel = . -}}
{{- end -}}
{{- end -}}

{{- $webURL = printf "https://%s/%s" $host $repoPath -}}

{{- end -}}{{- /* end with $host */ -}}

{{- end -}}{{- /* end with $forgeRaw */ -}}

{{- return (dict
"host" $host
"repoPath" $repoPath
"platform" $platform
"platformLabel" $platformLabel
"webURL" $webURL
"isLegacyGithub" $isLegacyGithub) -}}
Loading
Loading