diff --git a/assets/css/main.css b/assets/css/main.css index 0d51eb4..24bc033 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -412,9 +412,28 @@ main { padding-bottom: 6rem; } .post__body h3 { margin-top: 2rem; font-size: 1.1rem; } .post__body img { max-width: 100%; height: auto; } +/* ---- repo-card shortcodes: github, gitlab, huggingface ---- */ +.post__body .repo-card { text-decoration: none; } +.repo-card { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--fg); +} +.repo-card:hover { border-color: var(--link); } +.repo-card:hover .repo-card__name { color: var(--link); } +.repo-card__icon { flex: none; margin-top: 0.15rem; color: var(--muted); font-size: 1.1rem; line-height: 1; } +.repo-card__main { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; } +.repo-card__name { font-family: var(--mono); font-weight: 600; } +.repo-card__desc { color: var(--muted); font-size: 0.9rem; } +.repo-card__meta { margin-left: auto; flex: none; display: flex; gap: 0.75rem; font-family: var(--mono); font-size: 0.8rem; color: var(--muted); white-space: nowrap; } + /* figures (markdown images) */ .post-figure { margin: 2rem 0; } -.post-figure img { display: block; width: 100%; border: 1px solid var(--border); border-radius: 4px; } +.post-figure img { display: block; width: 100%; border-radius: 4px; } .post-figure figcaption { margin-top: 0.5rem; font-family: var(--mono); @@ -422,6 +441,79 @@ main { padding-bottom: 6rem; } color: var(--muted); text-align: center; } +.post-figure__zoom { display: block; cursor: zoom-in; } + +/* click-to-zoom lightbox: pure CSS via :target, no JS */ +.lightbox { + display: none; + position: fixed; + inset: 0; + z-index: 100; + padding: 2rem; + align-items: center; + justify-content: center; +} +.lightbox:target { display: flex; } +.lightbox__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.85); cursor: zoom-out; } +.lightbox img { position: relative; max-width: 100%; max-height: 100%; border-radius: 4px; } + +/* code shortcode: imported file/URL, source path shown above the block */ +.code-import figcaption { margin-bottom: 0.4rem; font-family: var(--mono); font-size: 0.75rem; color: var(--muted); } +.code-import figcaption a:hover { color: var(--link); } + +/* swatches shortcode */ +.swatches { display: flex; flex-wrap: wrap; gap: 1rem; margin: 1.5rem 0; } +.swatches__item { display: flex; flex-direction: column; align-items: center; gap: 0.4rem; } +.swatches__chip { display: block; width: 3rem; height: 3rem; border-radius: 6px; border: 1px solid var(--border); } +.swatches__label { font-family: var(--mono); font-size: 0.75rem; color: var(--muted); } + +/* admonition shortcode */ +.admonition { + margin: 1.5rem 0; + padding: 0.9rem 1.1rem; + border-left: 3px solid var(--admonition-color, var(--c-blue)); + background: color-mix(in srgb, var(--admonition-color, var(--c-blue)) 10%, transparent); + border-radius: 4px; +} +.admonition--tip { --admonition-color: var(--c-green); } +.admonition--warning { --admonition-color: var(--c-orange); } +.admonition--danger { --admonition-color: var(--c-red); } +.admonition__title { margin: 0 0 0.4rem; font-weight: 600; color: var(--admonition-color, var(--c-blue)); } +.admonition__body > :first-child { margin-top: 0; } +.admonition__body > :last-child { margin-bottom: 0; } + +/* details shortcode — native
/, themed */ +.details { margin: 1.5rem 0; border: 1px solid var(--border); border-radius: 6px; padding: 0.75rem 1rem; } +.details summary { cursor: pointer; font-weight: 600; } +.details[open] summary { margin-bottom: 0.5rem; } +.details__body > :first-child { margin-top: 0; } +.details__body > :last-child { margin-bottom: 0; } + +/* badge shortcode */ +.badge { + display: inline-block; + padding: 0.15em 0.55em; + border: 1px solid var(--border); + border-radius: 999px; + font-family: var(--mono); + font-size: 0.75rem; + color: var(--muted); +} +.badge--red { border-color: var(--c-red); color: var(--c-red); } +.badge--orange { border-color: var(--c-orange); color: var(--c-orange); } +.badge--yellow { border-color: var(--c-yellow); color: var(--c-yellow); } +.badge--green { border-color: var(--c-green); color: var(--c-green); } +.badge--cyan { border-color: var(--c-cyan); color: var(--c-cyan); } +.badge--blue { border-color: var(--c-blue); color: var(--c-blue); } +.badge--purple { border-color: var(--c-purple); color: var(--c-purple); } +.badge--pink { border-color: var(--c-pink); color: var(--c-pink); } + +/* footnotes (goldmark markup, no shortcode needed) */ +.footnote-ref { text-decoration: none; } +.footnotes { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.9rem; color: var(--muted); } +.footnotes hr { display: none; } +.footnotes ol { padding-left: 1.25rem; } +.footnote-backref { text-decoration: none; margin-left: 0.3rem; } /* math (MathML) */ .post__body math { font-size: 1.05em; } @@ -476,6 +568,7 @@ main { padding-bottom: 6rem; } font-family: var(--mono); font-size: 0.85rem; } +.post__body iframe { border-radius: 6px; } .post__body code { font-family: var(--mono); font-size: 0.9em; } .post__body :not(pre) > code { padding: 0.1em 0.35em; diff --git a/exampleSite/content/posts/hello-world.md b/exampleSite/content/posts/hello-world.md index f11afd9..77c9017 100644 --- a/exampleSite/content/posts/hello-world.md +++ b/exampleSite/content/posts/hello-world.md @@ -18,6 +18,18 @@ pop of colour. Inline `code` looks like this. > A blockquote sits quietly to one side. +## A repo + +{{< github "gohugoio/hugo" >}} + +{{< gitlab "gitlab-org/gitlab" >}} + +{{< huggingface "bert-base-uncased" >}} + +## Swatches + +{{< swatches "#64748b" "#3b82f6" "#06b6d4" >}} + ## A diagram ```mermaid @@ -41,3 +53,59 @@ Inline, like $E = mc^2$, sits in the sentence. Display math gets its own line: $$\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}$$ That's it. + +## Imported code + +{{< code file="assets/js/console.js" lines="1-8" >}} + +## A gist + +{{< gist "octocat" "6cad326836d38bd3a7ae" >}} + +## A video + +{{< youtube dQw4w9WgXcQ >}} + +## Contact + +Questions? {{< mailto "hello@example.org" "Hello from the blog" "say hi" >}}. + +Default text: {{< mailto "hello@example.org" >}} + +## Admonitions + +{{< admonition >}}A plain note, no title override.{{< /admonition >}} + +{{< admonition type="tip" title="Pro tip" >}}Use `pnpm dev` for live reload while writing.{{< /admonition >}} + +{{< admonition type="warning" title="Careful" >}}This regenerates the fingerprinted bundle.{{< /admonition >}} + +{{< admonition type="danger" title="Don't" >}}Never commit `.env` files.{{< /admonition >}} + +## Details + +{{< details summary="Click to expand" >}} +Hidden content with **markdown** support, revealed via the native `
` element. +{{< /details >}} + +## Badges + +{{< badge "MIT" >}} {{< badge "v1.2.0" "blue" >}} {{< badge "beta" "orange" >}} + +## Package cards + +{{< npm "left-pad" >}} + +{{< crates "serde" >}} + +{{< pypi "requests" >}} + +## A Bluesky post + +{{< bluesky "https://bsky.app/profile/bsky.app/post/3mqcp5qjdfs26" >}} + +## A footnote + +Here's a claim that needs backing up.[^1] + +[^1]: This is the footnote content, rendered by goldmark natively. diff --git a/layouts/_default/_markup/render-image.html b/layouts/_default/_markup/render-image.html index ca484e6..e7ec9d3 100644 --- a/layouts/_default/_markup/render-image.html +++ b/layouts/_default/_markup/render-image.html @@ -1,5 +1,13 @@ -{{/* Markdown images become
s; alt text doubles as caption. */}} +{{/* Markdown images become
s; alt text doubles as caption. + Click to zoom, a pure-CSS lightbox via :target — no JS. */}} +{{ $id := printf "img-%d" .Ordinal }}
- {{ .PlainText }} + + {{ .PlainText }} + {{ with .PlainText }}
{{ . }}
{{ end }}
+ diff --git a/layouts/shortcodes/admonition.html b/layouts/shortcodes/admonition.html new file mode 100644 index 0000000..1b39e95 --- /dev/null +++ b/layouts/shortcodes/admonition.html @@ -0,0 +1,8 @@ +{{- /* {{< admonition type="warning" title="Heads up" >}}markdown content{{< /admonition >}} + type: note (default) | tip | warning | danger. title is optional. */ -}} +{{- $type := .Get "type" | default "note" -}} +{{- $title := .Get "title" | default (title $type) -}} +
+

{{ $title }}

+
{{ .Inner | $.Page.RenderString }}
+
diff --git a/layouts/shortcodes/badge.html b/layouts/shortcodes/badge.html new file mode 100644 index 0000000..6314fed --- /dev/null +++ b/layouts/shortcodes/badge.html @@ -0,0 +1,7 @@ +{{- /* {{< badge "MIT" >}} or {{< badge "beta" color="orange" >}} + color: one of the pop palette names (red/orange/yellow/green/cyan/blue/purple/pink), defaults to accent. */ -}} +{{- $text := .Get 0 -}} +{{- if not $text }}{{ errorf "badge shortcode: text required, e.g. {{}} (%s)" .Position }}{{ end -}} +{{- $color := .Get 1 -}} +{{- with .Get "color" }}{{ $color = . }}{{ end -}} +{{ $text }} diff --git a/layouts/shortcodes/bluesky.html b/layouts/shortcodes/bluesky.html new file mode 100644 index 0000000..42b77fb --- /dev/null +++ b/layouts/shortcodes/bluesky.html @@ -0,0 +1,47 @@ +{{- /* {{< bluesky "https://bsky.app/profile/handle.bsky.social/post/postid" >}} + Fetches the post via Bluesky's public AppView API at build time and renders + it as a card — no embed script, no auth needed (unlike X/Twitter, which + removed unauthenticated read access, so there's no equivalent shortcode for + it without a developer API key). */ -}} +{{- $url := .Get 0 -}} +{{- if not $url }}{{ errorf "bluesky shortcode: post URL required, e.g. {{}} (%s)" .Position }}{{ end -}} +{{- $parts := strings.Split $url "/" -}} +{{- if lt (len $parts) 7 }}{{ errorf "bluesky shortcode: expected https://bsky.app/profile//post/ (%s)" .Position }}{{ end -}} +{{- $handle := index $parts 4 -}} +{{- $postID := index $parts 6 -}} + +{{- $atURI := printf "at://%s/app.bsky.feed.post/%s" $handle $postID -}} +{{- $encoded := replace $atURI ":" "%3A" -}} +{{- $encoded = replace $encoded "/" "%2F" -}} + +{{- $author := $handle -}} +{{- $text := "" -}} +{{- $likes := "" -}} +{{- $reposts := "" -}} +{{- $try := try (resources.GetRemote (printf "https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=%s" $encoded)) -}} +{{- with $try -}} + {{- if .Err }}{{ errorf "bluesky shortcode: failed to fetch %s: %s (%s)" $url .Err $.Position }} + {{- else -}} + {{- with .Value -}} + {{- $post := (transform.Unmarshal .Content).thread.post -}} + {{- $text = $post.record.text -}} + {{- $likes = int $post.likeCount -}} + {{- $reposts = int $post.repostCount -}} + {{- if $post.author.displayName }}{{ $author = printf "%s (@%s)" $post.author.displayName $post.author.handle }}{{ else }}{{ $author = $post.author.handle }}{{ end -}} + {{- end -}} + {{- end -}} +{{- end -}} + + + + + {{ $author }} + {{- with $text }}{{ . }}{{ end }} + + {{- if or $likes $reposts }} + + {{- with $reposts }}⟲ {{ . }}{{ end }} + {{- with $likes }}♥ {{ . }}{{ end }} + + {{- end }} + diff --git a/layouts/shortcodes/code.html b/layouts/shortcodes/code.html new file mode 100644 index 0000000..35642f2 --- /dev/null +++ b/layouts/shortcodes/code.html @@ -0,0 +1,40 @@ +{{- /* {{< code file="assets/js/console.js" >}} or {{< code url="https://raw.githubusercontent.com/…" >}} + Imports code from a local file or remote URL instead of pasting it inline. + Optional: lang="go" (else guessed from the extension), lines="12-34" (1-indexed, inclusive). */ -}} +{{- $file := .Get "file" -}} +{{- $url := .Get "url" -}} +{{- if not (or $file $url) }}{{ errorf "code shortcode: file or url required, e.g. {{}} (%s)" .Position }}{{ end -}} +{{- if and $file $url }}{{ errorf "code shortcode: pass file or url, not both (%s)" .Position }}{{ end -}} + +{{- $lang := .Get "lang" -}} +{{- $label := "" -}} +{{- $content := "" -}} +{{- if $file -}} + {{- $content = os.ReadFile $file -}} + {{- $label = $file -}} + {{- if not $lang }}{{ $lang = path.Ext $file | strings.TrimPrefix "." }}{{ end -}} +{{- else -}} + {{- $label = $url -}} + {{- $try := try (resources.GetRemote $url) -}} + {{- with $try -}} + {{- if .Err }}{{ errorf "code shortcode: failed to fetch %s: %s (%s)" $url .Err $.Position }} + {{- else -}} + {{- $content = .Value.Content -}} + {{- if not $lang }}{{ $lang = path.Ext $url | strings.TrimPrefix "." }}{{ end -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{- with .Get "lines" -}} + {{- $parts := strings.Split . "-" -}} + {{- $start := int (index $parts 0) -}} + {{- $end := $start -}} + {{- if gt (len $parts) 1 }}{{ $end = int (index $parts 1) }}{{ end -}} + {{- $all := strings.Split $content "\n" -}} + {{- $content = delimit (after (sub $start 1) (first $end $all)) "\n" -}} +{{- end -}} + +
+
{{ if $url }}{{ $label }}{{ else }}{{ $label }}{{ end }}
+ {{ transform.Highlight $content $lang }} +
diff --git a/layouts/shortcodes/crates.html b/layouts/shortcodes/crates.html new file mode 100644 index 0000000..cd02988 --- /dev/null +++ b/layouts/shortcodes/crates.html @@ -0,0 +1,38 @@ +{{- /* {{< crates "serde" >}} — card linking to a crates.io package. + Auto-fetches description/version/downloads at build time; pass desc="…" + version="…" downloads="…" to override (or skip the network call). */ -}} +{{- $pkg := .Get 0 -}} +{{- with .Get "pkg" }}{{ $pkg = . }}{{ end -}} +{{- if not $pkg }}{{ errorf "crates shortcode: crate name required, e.g. {{}} (%s)" .Position }}{{ end -}} + +{{- $desc := .Get "desc" -}} +{{- $version := .Get "version" -}} +{{- $downloads := .Get "downloads" -}} +{{- if not (and $desc $version $downloads) -}} + {{- $opts := dict "headers" (dict "User-Agent" "basic-hugo-theme (https://github.com/jjsnack/basic)") -}} + {{- $try := try (resources.GetRemote (printf "https://crates.io/api/v1/crates/%s" $pkg) $opts) -}} + {{- with $try -}} + {{- if not .Err -}} + {{- with .Value -}} + {{- $data := (transform.Unmarshal .Content).crate -}} + {{- if not $desc }}{{ $desc = $data.description }}{{ end -}} + {{- if not $version }}{{ $version = $data.max_stable_version }}{{ end -}} + {{- if not $downloads }}{{ $downloads = int $data.downloads }}{{ end -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + + + + + {{ $pkg }} + {{- with $desc }}{{ . }}{{ end }} + + {{- if or $version $downloads }} + + {{- with $version }}v{{ . }}{{ end }} + {{- with $downloads }}⬇ {{ . }}{{ end }} + + {{- end }} + diff --git a/layouts/shortcodes/details.html b/layouts/shortcodes/details.html new file mode 100644 index 0000000..921365a --- /dev/null +++ b/layouts/shortcodes/details.html @@ -0,0 +1,7 @@ +{{- /* {{< details summary="Click to expand" >}}markdown content{{< /details >}} + Wraps the native
/ disclosure widget — no JS. */ -}} +{{- $summary := .Get "summary" | default "Details" -}} +
+ {{ $summary }} +
{{ .Inner | $.Page.RenderString }}
+
diff --git a/layouts/shortcodes/gist.html b/layouts/shortcodes/gist.html new file mode 100644 index 0000000..0c55990 --- /dev/null +++ b/layouts/shortcodes/gist.html @@ -0,0 +1,21 @@ +{{- /* {{< gist "user" "gist-id" >}} — embeds a GitHub gist. + Hugo's built-in gist shortcode was removed in v0.156; this fetches the + gist via the GitHub API at build time and renders it with the site's own + Chroma highlighting instead of GitHub's unstyled diff --git a/layouts/shortcodes/npm.html b/layouts/shortcodes/npm.html new file mode 100644 index 0000000..fae55dc --- /dev/null +++ b/layouts/shortcodes/npm.html @@ -0,0 +1,37 @@ +{{- /* {{< npm "left-pad" >}} — card linking to an npm package. + Auto-fetches description/version/license from the registry at build time; + pass desc="…" version="…" license="…" to override (or skip the network call). */ -}} +{{- $pkg := .Get 0 -}} +{{- with .Get "pkg" }}{{ $pkg = . }}{{ end -}} +{{- if not $pkg }}{{ errorf "npm shortcode: package name required, e.g. {{}} (%s)" .Position }}{{ end -}} + +{{- $desc := .Get "desc" -}} +{{- $version := .Get "version" -}} +{{- $license := .Get "license" -}} +{{- if not (and $desc $version $license) -}} + {{- $try := try (resources.GetRemote (printf "https://registry.npmjs.org/%s" $pkg)) -}} + {{- with $try -}} + {{- if not .Err -}} + {{- with .Value -}} + {{- $data := transform.Unmarshal .Content -}} + {{- if not $desc }}{{ $desc = $data.description }}{{ end -}} + {{- if not $version }}{{ $version = index $data "dist-tags" "latest" }}{{ end -}} + {{- if not $license }}{{ $license = $data.license }}{{ end -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + + + + + {{ $pkg }} + {{- with $desc }}{{ . }}{{ end }} + + {{- if or $version $license }} + + {{- with $version }}v{{ . }}{{ end }} + {{- with $license }}{{ . }}{{ end }} + + {{- end }} + diff --git a/layouts/shortcodes/pypi.html b/layouts/shortcodes/pypi.html new file mode 100644 index 0000000..fb749d6 --- /dev/null +++ b/layouts/shortcodes/pypi.html @@ -0,0 +1,37 @@ +{{- /* {{< pypi "requests" >}} — card linking to a PyPI package. + Auto-fetches summary/version/license at build time; pass desc="…" + version="…" license="…" to override (or skip the network call). */ -}} +{{- $pkg := .Get 0 -}} +{{- with .Get "pkg" }}{{ $pkg = . }}{{ end -}} +{{- if not $pkg }}{{ errorf "pypi shortcode: package name required, e.g. {{}} (%s)" .Position }}{{ end -}} + +{{- $desc := .Get "desc" -}} +{{- $version := .Get "version" -}} +{{- $license := .Get "license" -}} +{{- if not (and $desc $version $license) -}} + {{- $try := try (resources.GetRemote (printf "https://pypi.org/pypi/%s/json" $pkg)) -}} + {{- with $try -}} + {{- if not .Err -}} + {{- with .Value -}} + {{- $data := (transform.Unmarshal .Content).info -}} + {{- if not $desc }}{{ $desc = $data.summary }}{{ end -}} + {{- if not $version }}{{ $version = $data.version }}{{ end -}} + {{- if not $license }}{{ $license = $data.license }}{{ end -}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- end -}} + + + + + {{ $pkg }} + {{- with $desc }}{{ . }}{{ end }} + + {{- if or $version $license }} + + {{- with $version }}v{{ . }}{{ end }} + {{- with $license }}{{ . }}{{ end }} + + {{- end }} + diff --git a/layouts/shortcodes/swatches.html b/layouts/shortcodes/swatches.html new file mode 100644 index 0000000..3e7c012 --- /dev/null +++ b/layouts/shortcodes/swatches.html @@ -0,0 +1,10 @@ +{{- /* {{< swatches "#64748b" "#3b82f6" "#06b6d4" >}} — a row of colour chips, each labelled with the value passed. */ -}} +{{- if eq (len .Params) 0 }}{{ errorf "swatches shortcode: pass one or more colours, e.g. {{}} (%s)" .Position }}{{ end -}} +
+ {{- range .Params }} +
+ + {{ . }} +
+ {{- end }} +