From caee92bfc70f79589646d0d7d047e06ec880cacc Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Wed, 18 Feb 2026 23:57:07 +0100 Subject: [PATCH 1/9] Refactor development setup: update local server instructions and remove deprecated server script --- CLAUDE.md | 10 +++++----- server.sh | 27 --------------------------- 2 files changed, 5 insertions(+), 32 deletions(-) delete mode 100755 server.sh diff --git a/CLAUDE.md b/CLAUDE.md index 719259fc..90a87a31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,13 +58,13 @@ static/ # Favicons, fonts (woff2), social SVG icons ## Development -**Local server** (two options): -- `./server.sh` — runs Hugo directly, binds to LAN IP or localhost, builds drafts and future posts -- `docker compose up` — runs Hugo in Docker container on port 8080 (matches CI environment) +**Local server**: `docker compose up` — runs Hugo in a Docker container (`ghcr.io/vasylenko/hugo-runtime`) on port 8080. Builds drafts and future posts, disables fast render for reliable live reload. Hugo version is pinned in `.env` and shared with CI. -**New blog post**: `./newpost.sh ` — scaffolds a page bundle from the `post-bundle` archetype +**New blog post**: `./newpost.sh ` — scaffolds a page bundle under `content/posts/` from the `post-bundle` archetype (requires local Hugo install) -**Hugo environment configs**: `development` is used locally (drafts, no minification), `production` is used in CI (base URL, minification, analytics injection) +**Hugo environment configs**: `development` is used locally (drafts enabled, no minification), `production` is used in CI (real base URL, minification, Simple Analytics injection) + +**Docker image** (`hugo-runtime.dockerfile`): Two-stage build — Alpine downloads the Hugo extended binary, copies it into a `golang:1.22` image (Go is needed for Hugo modules). Published to `ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}`. Rebuilt automatically by CI when the Dockerfile, compose file, or `.env` changes. ## Content Conventions diff --git a/server.sh b/server.sh deleted file mode 100755 index e5e41619..00000000 --- a/server.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/zsh -bindip=$(ipconfig getifaddr en0) -site_env=development - -for arg in "$@" -do -case $arg in - localhost) - bindip=localhost - shift - ;; - production) - site_env=production - shift - ;; - development) - site_env=development - shift - ;; - *) - # unknown option - echo -e "Unknown option $arg" - exit 2 - ;; -esac -done -hugo server --bind ${bindip} --baseURL http://${bindip} --buildDrafts --buildFuture --environment $site_env --gc --noHTTPCache --disableFastRender \ No newline at end of file From e2686f74a6d10b302f3484d6f7ad1fe63e02f2a2 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 09:59:10 +0100 Subject: [PATCH 2/9] Upgrade build toolchain: Hugo 0.156.0, Go 1.25, PaperMod Jan 2026 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full dependency refresh after 12 months dormancy: - Hugo 0.143.1 → 0.156.0, Go 1.22 → 1.25, PaperMod Feb 2025 → Jan 2026 - Dockerfile: pin Alpine 3.23, fix archive naming (linux-amd64), fix tar.gz typo - GitHub Actions: bump checkout@v6, upload-artifact@v6, download-artifact@v7 - Fix CSS syntax error in minimalistic-blog-items-list.css (stray = char) - Fix font format declarations: format('woff') → format('woff2') for woff2 files - Fix deprecated minify config: keepConditionalComments → keepSpecialComments - Remove deprecated kind front matter from about page - Inline gist content to replace deprecated gist shortcode - Drop cover.html override in favor of upstream (adds lazy loading, figcaption) - Align single.html override with upstream indentation (keep hooks) - Remove dead code: Mailchimp partial, heapanalyticsid config Created with Claude Code under the supervision of Serhii Vasylenko --- .env | 2 +- .github/workflows/build-hugo-image.yaml | 2 +- .github/workflows/deploy-preview.yaml | 2 +- .github/workflows/deploy-production.yaml | 9 +- .../extended/minimalistic-blog-items-list.css | 2 +- config/production/config.yaml | 4 +- content/about/index.md | 1 - .../index.md | 32 ++++- go.mod | 4 +- go.sum | 6 +- hugo-runtime.dockerfile | 14 +-- layouts/_default/single.html | 118 +++++++++--------- layouts/partials/cover.html | 40 ------ .../partials/email-subscribe.html_mailchimp | 34 ----- layouts/partials/extend_head.html | 16 +-- 15 files changed, 119 insertions(+), 167 deletions(-) delete mode 100644 layouts/partials/cover.html delete mode 100644 layouts/partials/email-subscribe.html_mailchimp diff --git a/.env b/.env index db5f3702..929a9e80 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -HUGO_VERSION=0.143.1 \ No newline at end of file +HUGO_VERSION=0.156.0 \ No newline at end of file diff --git a/.github/workflows/build-hugo-image.yaml b/.github/workflows/build-hugo-image.yaml index 6559b0d9..1766f1f7 100644 --- a/.github/workflows/build-hugo-image.yaml +++ b/.github/workflows/build-hugo-image.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Authenticate to GitHub Container Registry run: echo "${{ github.token }}" | docker login ghcr.io -u $ --password-stdin - name: Build and push Docker image diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml index 05a35558..187b9437 100644 --- a/.github/workflows/deploy-preview.yaml +++ b/.github/workflows/deploy-preview.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build Hugo Website uses: ./.github/actions/build-hugo-website diff --git a/.github/workflows/deploy-production.yaml b/.github/workflows/deploy-production.yaml index eba0a35b..f8767277 100644 --- a/.github/workflows/deploy-production.yaml +++ b/.github/workflows/deploy-production.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Build Hugo Website uses: ./.github/actions/build-hugo-website @@ -24,7 +24,7 @@ jobs: build-artifact: ${{ env.ARTIFACT }} - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ env.ARTIFACT }} path: ${{ env.ARTIFACT }} @@ -39,16 +39,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the branch for website hosting - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: ref: '${{ env.GHPAGES_BRANCH }}' - name: Get the new website content - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: ${{ env.ARTIFACT }} path: ./ - retention-days: 1 - name: Deploy website run: | diff --git a/assets/css/extended/minimalistic-blog-items-list.css b/assets/css/extended/minimalistic-blog-items-list.css index b0105ee1..fdd6882c 100644 --- a/assets/css/extended/minimalistic-blog-items-list.css +++ b/assets/css/extended/minimalistic-blog-items-list.css @@ -21,7 +21,7 @@ } .post-entry figure.entry-cover img { width: 300px; -= } + } .post-entry div.entry-content { grid-area: content; diff --git a/config/production/config.yaml b/config/production/config.yaml index 20e58e0f..73616d4f 100644 --- a/config/production/config.yaml +++ b/config/production/config.yaml @@ -8,7 +8,7 @@ minify: tdewolff: html: keepComments: false - keepConditionalComments: true + keepSpecialComments: true keepDefaultAttrVals: true keepDocumentTags: true keepEndTags: true @@ -17,5 +17,5 @@ minify: params: env: production # to enable google analytics, opengraph, twitter-cards and schema. - heapanalyticsid: 2300872157 + EmailSubscribeForm: true diff --git a/content/about/index.md b/content/about/index.md index e8fe5057..210ceab8 100644 --- a/content/about/index.md +++ b/content/about/index.md @@ -9,7 +9,6 @@ hideSummary: true cover.hidden: true ShowBreadCrumbs: false title: About me — Serhii Vasylenko -kind: page ShowLicense: false EmailSubscribeForm: false --- diff --git a/content/posts/2021/image-compression-with-tinypng-from-macos-contextual-menu/index.md b/content/posts/2021/image-compression-with-tinypng-from-macos-contextual-menu/index.md index 7b43fb21..ef491f10 100644 --- a/content/posts/2021/image-compression-with-tinypng-from-macos-contextual-menu/index.md +++ b/content/posts/2021/image-compression-with-tinypng-from-macos-contextual-menu/index.md @@ -53,7 +53,37 @@ Click the **Option** button at the bottom of the Action window and **Uncheck** ` Put the following script into the **Run Shell Script** window, replacing the *YOUR_API_KEY_HERE* string with your API key obtained from TinyPNG. -{{< gist vasylenko 13cb423aa83265e79ac5ad900195603f >}} +```zsh +# MacOS Automator Quick Action for image compression using Tinypng.com +# You need to register to obtain a personal API key +# https://tinypng.com/developers +# +# The code below should be put inside "Run Shell Script" Action with the following configuration in the Automator: +# workflow receives current: files or folders | in Finder +# shell: /bin/zsh +# pass input: as arguments + +set -e -o pipefail +export PATH="$PATH:/usr/local/bin" +APIKEY=YOUR_API_KEY_HERE +tinypng () { + file_name="$1:t:r" + file_ext="$1:t:e" + file_dir="$1:h" + compressed_url="$(curl -D - -o /dev/null --user api:$APIKEY --data-binary @"$1" https://api.tinify.com/shrink|grep location|cut -d ' ' -f 2|sed 's/\r//')" + curl -o "${file_dir}/${file_name}_compressed.${file_ext}" "$compressed_url" +} + +for f in "$@" +do + if [ -f "$f" ]; then + tinypng "$f" + elif [ -d "$f" ]; then + find "$f" -name "*.png" -o -name "*.jpeg" -o -name "*.jpg" | while read file_name; do + tinypng "$file_name"; done + fi +done +``` ## Utilities used in the script — explained diff --git a/go.mod b/go.mod index 4629c8f1..b7142d63 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/vasylenko/devdosvid.blog.git -go 1.22.1 +go 1.25.0 -require github.com/adityatelange/hugo-PaperMod v0.0.0-20250222173341-243ba38a34ce // indirect +require github.com/adityatelange/hugo-PaperMod v0.0.0-20260125152547-3bb0ca281fd1 // indirect diff --git a/go.sum b/go.sum index 2dbdec72..06b4a2fe 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,2 @@ -github.com/adityatelange/hugo-PaperMod v0.0.0-20250105143455-9f1f414be805 h1:gi6q4K4XngQJj1oCHJvIT9huDaW1PDcSvVR8/amBxLE= -github.com/adityatelange/hugo-PaperMod v0.0.0-20250105143455-9f1f414be805/go.mod h1:HCHxNMKYdGafUYjVV3ICiAqznZK2yH0iI53jqcDFDdQ= -github.com/adityatelange/hugo-PaperMod v0.0.0-20250222173341-243ba38a34ce h1:sS4w+tPrfbN8B+toyGwOhGyT2G64w6V6HLfs2ciM218= -github.com/adityatelange/hugo-PaperMod v0.0.0-20250222173341-243ba38a34ce/go.mod h1:HCHxNMKYdGafUYjVV3ICiAqznZK2yH0iI53jqcDFDdQ= +github.com/adityatelange/hugo-PaperMod v0.0.0-20260125152547-3bb0ca281fd1 h1:nS0GAIWJZIxy1YWwGce18ePQHB7v1Y1sxUTOOrBAIeA= +github.com/adityatelange/hugo-PaperMod v0.0.0-20260125152547-3bb0ca281fd1/go.mod h1:HCHxNMKYdGafUYjVV3ICiAqznZK2yH0iI53jqcDFDdQ= diff --git a/hugo-runtime.dockerfile b/hugo-runtime.dockerfile index 1f7417e4..0261c463 100644 --- a/hugo-runtime.dockerfile +++ b/hugo-runtime.dockerfile @@ -1,17 +1,17 @@ -FROM alpine:latest as builder +FROM alpine:3.23 AS builder ARG HUGO_VERSION WORKDIR /hugo RUN wget -q -c \ - "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz" \ - -O hugo.tag.gz -RUN tar -xzf hugo.tag.gz + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ + -O hugo.tar.gz +RUN tar -xzf hugo.tar.gz -FROM golang:1.22 +FROM golang:1.25 ARG HUGO_VERSION COPY --from=builder /hugo/hugo / WORKDIR /site ENTRYPOINT ["/hugo"] LABEL org.opencontainers.image.source=https://github.com/vasylenko/devdosvid.blog -LABEL org.opencontainers.image.description="Official Hugo v${HUGO_VERSION} binary in Golang v1.22 Image to run or build a hugo website." -LABEL org.opencontainers.image.licenses=MIT \ No newline at end of file +LABEL org.opencontainers.image.description="Official Hugo v${HUGO_VERSION} binary in Golang v1.25 Image to run or build a hugo website." +LABEL org.opencontainers.image.licenses=MIT diff --git a/layouts/_default/single.html b/layouts/_default/single.html index b74e149a..24473d83 100644 --- a/layouts/_default/single.html +++ b/layouts/_default/single.html @@ -1,71 +1,71 @@ {{- define "main" }}
-
- {{ partial "breadcrumbs.html" . }} -

- {{ .Title }} - {{- if .Draft }} - - - - - - {{- end }} -

- {{- if .Description }} -
- {{ .Description }} -
- {{- end }} - {{- if not (.Param "hideMeta") }} - - {{- end }} -
- {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }} - {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }} - {{- if (.Param "ShowToc") }} - {{- partial "toc.html" . }} +
+ {{ partial "breadcrumbs.html" . }} +

+ {{ .Title }} + {{- if .Draft }} + + + + + + {{- end }} +

+ {{- if .Description }} +
+ {{ .Description }} +
{{- end }} - - {{- /* Hook: Beginning of post content */ -}} - {{- partial "hooks/post-content-begin.html" . -}} - - {{- if .Content }} -
- {{- if not (.Param "disableAnchoredHeadings") }} - {{- partial "anchored_headings.html" .Content -}} - {{- else }}{{ .Content }}{{ end }} + {{- if not (.Param "hideMeta") }} + {{- end }} +
+ {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }} + {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }} + {{- if (.Param "ShowToc") }} + {{- partial "toc.html" . }} + {{- end }} - {{- /* Hook: End of post content */ -}} - {{- partial "hooks/post-content-end.html" . -}} + {{- /* Hook: Beginning of post content */ -}} + {{- partial "hooks/post-content-begin.html" . -}} -
- {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }} - - {{- if (.Param "ShowPostNavLinks") }} - {{- partial "post_nav_links.html" . }} - {{- end }} - {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }} - {{- partial "share_icons.html" . -}} - {{- end }} -
+ {{- if .Content }} +
+ {{- if not (.Param "disableAnchoredHeadings") }} + {{- partial "anchored_headings.html" .Content -}} + {{- else }}{{ .Content }}{{ end }} +
+ {{- end }} - {{- if (.Param "comments") }} - {{- partial "comments.html" . }} + {{- /* Hook: End of post content */ -}} + {{- partial "hooks/post-content-end.html" . -}} + +
+ {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }} + + {{- if (.Param "ShowPostNavLinks") }} + {{- partial "post_nav_links.html" . }} + {{- end }} + {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }} + {{- partial "share_icons.html" . -}} {{- end }} +
+ + {{- if (.Param "comments") }} + {{- partial "comments.html" . }} + {{- end }}
-{{- end }}{{/* end main */}} \ No newline at end of file +{{- end }}{{/* end main */}} diff --git a/layouts/partials/cover.html b/layouts/partials/cover.html deleted file mode 100644 index 4c8b0799..00000000 --- a/layouts/partials/cover.html +++ /dev/null @@ -1,40 +0,0 @@ -{{- with .cxt}} {{/* Apply proper context from dict */}} -{{- if (and .Params.cover.image (not $.isHidden)) }} -{{- $alt := (.Params.cover.alt | default .Params.cover.caption | plainify) }} -
- {{- $responsiveImages := (.Params.cover.responsiveImages | default site.Params.cover.responsiveImages) | default true }} - {{- $addLink := (and site.Params.cover.linkFullImages (not $.IsHome)) }} - {{- $cover := (.Resources.ByType "image").GetMatch (printf "*%s*" (.Params.cover.image)) }} - {{- if $cover -}}{{/* i.e it is present in page bundle */}} - {{- if $addLink }}{{ end -}} - {{- $sizes := (slice "360" "480" "720" "1080" "1500") }} - {{- $processableFormats := (slice "jpg" "jpeg" "png" "tif" "bmp" "gif") -}} - {{- if hugo.IsExtended -}} - {{- $processableFormats = $processableFormats | append "webp" -}} - {{- end -}} - {{- $prod := (hugo.IsProduction | or (eq site.Params.env "production")) }} - {{- if (and (in $processableFormats $cover.MediaType.SubType) ($responsiveImages) (eq $prod true)) }} - {{ $alt }} - {{- else }}{{/* Unprocessable image or responsive images disabled */}} - {{ $alt }} - {{- end }} - {{- else }}{{/* For absolute urls and external links, no img processing here */}} - {{- if $addLink }}{{ end -}} - {{ $alt }} - {{- end }} - {{- if $addLink }}{{ end -}} - {{/* Display Caption */}} - {{- if not $.IsHome }} - {{ with .Params.cover.caption }}

{{ . | markdownify }}

{{- end }} - {{- end }} -
-{{- end }}{{/* End image */}} -{{- end -}}{{/* End context */ -}} diff --git a/layouts/partials/email-subscribe.html_mailchimp b/layouts/partials/email-subscribe.html_mailchimp deleted file mode 100644 index 8e6c8348..00000000 --- a/layouts/partials/email-subscribe.html_mailchimp +++ /dev/null @@ -1,34 +0,0 @@ - -
-
-
-

Subscribe to devDosvid blog!

-
- - - -
- -
- - -
- - -
-
- -
-
-
-
-
- - \ No newline at end of file diff --git a/layouts/partials/extend_head.html b/layouts/partials/extend_head.html index 47b0d1d1..290d8cf4 100644 --- a/layouts/partials/extend_head.html +++ b/layouts/partials/extend_head.html @@ -4,7 +4,7 @@ font-style: normal; font-display: swap; font-weight: 300; - src: url(/assets/fonts/AlbertSans-Light.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-Light.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -12,7 +12,7 @@ font-style: italic; font-display: swap; font-weight: 300; - src: url(/assets/fonts/AlbertSans-LightItalic.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-LightItalic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -20,7 +20,7 @@ font-style: normal; font-display: swap; font-weight: 400; - src: url(/assets/fonts/AlbertSans-Regular.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-Regular.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -28,7 +28,7 @@ font-style: italic; font-display: swap; font-weight: 400; - src: url(/assets/fonts/AlbertSans-Italic.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-Italic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -36,7 +36,7 @@ font-style: normal; font-display: swap; font-weight: 500; - src: url(/assets/fonts/AlbertSans-Medium.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-Medium.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -44,7 +44,7 @@ font-style: italic; font-display: swap; font-weight: 500; - src: url(/assets/fonts/AlbertSans-MediumItalic.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-MediumItalic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -52,7 +52,7 @@ font-style: normal; font-display: swap; font-weight: 600; - src: url(/assets/fonts/AlbertSans-SemiBold.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-SemiBold.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { @@ -60,7 +60,7 @@ font-style: italic; font-display: swap; font-weight: 600; - src: url(/assets/fonts/AlbertSans-SemiBoldItalic.woff2) format('woff'); + src: url(/assets/fonts/AlbertSans-SemiBoldItalic.woff2) format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } From 2bfee1483f119cc1460ec307b7cbf7313f0aafa8 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 10:12:42 +0100 Subject: [PATCH 3/9] Remove email subscription banners from all articles Remove the Beehiiv email subscription form from blog posts: - Remove 9 inline {{< email-subscription >}} shortcode usages from posts - Remove post-content-end hook that injected the form after every post - Delete shortcode, component partial, CSS, and hook files - Drop single.html override (only existed for the now-empty hooks) - Remove EmailSubscribeForm config param from dev/prod configs - Remove EmailSubscribeForm front matter from about and cv pages - Update CLAUDE.md shortcode reference table Created with Claude Code under the supervision of Serhii Vasylenko --- CLAUDE.md | 1 - .../css/extended/email-subscription-form.css | 23 ------ config/development/config.yaml | 1 - config/production/config.yaml | 2 - content/about/index.md | 1 - content/cv/index.md | 1 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 - .../index.md | 2 +- layouts/_default/single.html | 71 ------------------- .../email-subscription-content.html | 7 -- .../partials/hooks/post-content-begin.html | 8 --- layouts/partials/hooks/post-content-end.html | 10 --- layouts/shortcodes/email-subscription.html | 1 - 20 files changed, 1 insertion(+), 143 deletions(-) delete mode 100644 assets/css/extended/email-subscription-form.css delete mode 100644 layouts/_default/single.html delete mode 100644 layouts/partials/components/email-subscription-content.html delete mode 100644 layouts/partials/hooks/post-content-begin.html delete mode 100644 layouts/partials/hooks/post-content-end.html delete mode 100644 layouts/shortcodes/email-subscription.html diff --git a/CLAUDE.md b/CLAUDE.md index 90a87a31..d61ffcdf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -86,7 +86,6 @@ static/ # Favicons, fonts (woff2), social SVG icons | `{{< youtube src="VIDEO_ID" title="..." >}}` | Responsive YouTube embed | | `{{< animation src="..." >}}` | Looping webm video | | `{{< tech-talk title="..." event="..." date="..." >}}` | Tech talk card (about page) | -| `{{< email-subscription >}}` | Beehiiv subscription form | | `{{< rawhtml >}}...{{< /rawhtml >}}` | Raw HTML passthrough | ## Code Style & Conventions diff --git a/assets/css/extended/email-subscription-form.css b/assets/css/extended/email-subscription-form.css deleted file mode 100644 index edd833cd..00000000 --- a/assets/css/extended/email-subscription-form.css +++ /dev/null @@ -1,23 +0,0 @@ -.beehiiv-subscribe-container { - background: var(--color-background); - width: 100%; - border: none; - text-align: center; -} - -.beehiiv-subscribe-container h3 { - font-weight: normal; - font-size: 1.5em; - padding-bottom: 0; - margin-bottom: 5px; - margin-top: 0; -} - -.beehiiv-subscribe-container iframe { - width: 100%; -} - -.beehiiv-subscribe-container p { - margin: 0; - font-style: italic; -} \ No newline at end of file diff --git a/config/development/config.yaml b/config/development/config.yaml index eaca3ccb..450ecad1 100644 --- a/config/development/config.yaml +++ b/config/development/config.yaml @@ -3,7 +3,6 @@ buildFuture: true buildExpired: true params: env: development - EmailSubscribeForm: true minify: disableCSS: false disableHTML: false diff --git a/config/production/config.yaml b/config/production/config.yaml index 73616d4f..302adf5b 100644 --- a/config/production/config.yaml +++ b/config/production/config.yaml @@ -17,5 +17,3 @@ minify: params: env: production # to enable google analytics, opengraph, twitter-cards and schema. - - EmailSubscribeForm: true diff --git a/content/about/index.md b/content/about/index.md index 210ceab8..237f1654 100644 --- a/content/about/index.md +++ b/content/about/index.md @@ -10,7 +10,6 @@ cover.hidden: true ShowBreadCrumbs: false title: About me — Serhii Vasylenko ShowLicense: false -EmailSubscribeForm: false --- Hello! I am Serhii, a software engineer by day and a tech enthusiast by night, currently crafting DevSecOps magic at [Grammarly](https://www.grammarly.com/). diff --git a/content/cv/index.md b/content/cv/index.md index 5c8bd7a3..5302cbcc 100644 --- a/content/cv/index.md +++ b/content/cv/index.md @@ -13,7 +13,6 @@ keywords: [Staff Engineer, Platform Engineering, Security Automation, Developer summary: "Serhii Vasylenko — professional experience" title: "Serhii Vasylenko" ShowLicense: false -EmailSubscribeForm: false --- Staff Engineer delivering strategic improvements across platform scalability, security posture, and developer productivity. Proven expertise in architecting and leading complex initiatives, such as reducing critical vulnerabilities by 59% across 70+ services (CISO priority) and saving $500K+ annually via CI/CD optimization (Kubernetes). My impact bridges infrastructure and security domains, balancing robust system design with operational excellence and developer velocity. I am a passionate mentor dedicated to building high-performing teams and enabling engineers to lead independently. diff --git a/content/posts/2020/2020-09-09-terraform-modules-explained/index.md b/content/posts/2020/2020-09-09-terraform-modules-explained/index.md index 96f0fa01..53308f7d 100644 --- a/content/posts/2020/2020-09-09-terraform-modules-explained/index.md +++ b/content/posts/2020/2020-09-09-terraform-modules-explained/index.md @@ -19,8 +19,6 @@ I assume you already know some basics about Terraform or even tried to use it in Please note: I do not use real code examples with some specific provider like AWS or Google intentionally, just for the sake of simplicity. -{{}} - ## Terraform modules You already write modules even if you think you don’t. diff --git a/content/posts/2021/2021-01-19-mac1-metal-EC2-Instance-user-experience/index.md b/content/posts/2021/2021-01-19-mac1-metal-EC2-Instance-user-experience/index.md index 91f6a294..8b2752d4 100644 --- a/content/posts/2021/2021-01-19-mac1-metal-EC2-Instance-user-experience/index.md +++ b/content/posts/2021/2021-01-19-mac1-metal-EC2-Instance-user-experience/index.md @@ -40,8 +40,6 @@ Some basic information about the Mac EC2 first: - You don't pay anything for the Instance itself, but you pay for the Dedicated Host leasing, and the minimum lease time is 24 hours. -{{}} - ## EC2 Mac Instance Prices (June 2022) On-demand pricing (us-east-1, North Virginia: - mac1.metal costs 1.083 USD per hour or about 780 USD per month diff --git a/content/posts/2021/2021-05-21-configure-http-security-headers-with-cloudfront-functions/index.md b/content/posts/2021/2021-05-21-configure-http-security-headers-with-cloudfront-functions/index.md index 728c70ab..f65e0285 100644 --- a/content/posts/2021/2021-05-21-configure-http-security-headers-with-cloudfront-functions/index.md +++ b/content/posts/2021/2021-05-21-configure-http-security-headers-with-cloudfront-functions/index.md @@ -26,8 +26,6 @@ It is “true edge” because Functions work on 200+ edge locations ([link to do One of the use cases for Lambda@Edge was adding security HTTP headers (it’s even listed on the [product page](https://aws.amazon.com/lambda/edge/)), and now there is one more way to make it using CloudFront Functions. -{{}} - ## What are security headers, and why it matters Security Headers are one of the web security pillars. diff --git a/content/posts/2022/new-lifecycle-options-and-refactoring-capabilities-in-terraform-1-1-and-1-2/index.md b/content/posts/2022/new-lifecycle-options-and-refactoring-capabilities-in-terraform-1-1-and-1-2/index.md index ce9cda8f..c10a1605 100644 --- a/content/posts/2022/new-lifecycle-options-and-refactoring-capabilities-in-terraform-1-1-and-1-2/index.md +++ b/content/posts/2022/new-lifecycle-options-and-refactoring-capabilities-in-terraform-1-1-and-1-2/index.md @@ -20,8 +20,6 @@ It's been only a few months since Terraform 1.1 was released with the `moved` bl Now Terraform 1.2 is almost [ready](*https://github.com/hashicorp/terraform/releases/tag/v1.2.0-rc1*) (as I am writing this blog in early May 2022) to bring three new efficient controls to the resource lifecycle.\ These are three new expressions: `precondition`, `postcondition`, and `replace_triggered_by`. -{{}} - ## Terraform Code Refactoring With the Moved Block Starting from the 1.1 version, Terraform users can use the `moved` block to describe the changes in resource or module addresses (or resources inside a module) in the form of code. \ Once that is described, Terraform performs the movement of the resource within the state during the first apply. diff --git a/content/posts/2022/some-techniques-to-enhance-your-terraform-proficiency/index.md b/content/posts/2022/some-techniques-to-enhance-your-terraform-proficiency/index.md index 34048f33..08c095d8 100644 --- a/content/posts/2022/some-techniques-to-enhance-your-terraform-proficiency/index.md +++ b/content/posts/2022/some-techniques-to-enhance-your-terraform-proficiency/index.md @@ -99,8 +99,6 @@ For example, here is how I would reference a resource created in the module with bucket_name = module.bucket["photos"].name ``` -{{}} - ## Conditional resource arguments (attributes) setting {{
}} diff --git a/content/posts/2023/hello-terraform-data-goodbye-null-resource/index.md b/content/posts/2023/hello-terraform-data-goodbye-null-resource/index.md index 3a8c4bf6..f16e50fd 100644 --- a/content/posts/2023/hello-terraform-data-goodbye-null-resource/index.md +++ b/content/posts/2023/hello-terraform-data-goodbye-null-resource/index.md @@ -18,8 +18,6 @@ In this blog post, I want to demonstrate and explain the **terraform_data** reso - firstly, it allows arbitrary values to be stored and used afterward to implement lifecycle triggers of other resources - secondly, it can be used to trigger provisioners when there isn't a more appropriate managed resource available. -{{}} - For those of you, who are familiar with the null provider, the `terraform_data` resource might look very similar. And you're right!\ Rather than being a separate provider, the terraform_data managed resource now offers the same capabilities as an integrated feature. Pretty cool! \ While the null provider is still available and there are no statements about its deprecation thus far ([as of April 2023, v3.2.1](https://registry.terraform.io/providers/hashicorp/null/3.2.1/docs)), the `terraform_data` is the native replacement of the `null_resource`, and the latter might soon become deprecated. diff --git a/content/posts/2024/A-Deep-Dive-Into-Terraform-Static-Code-Analysis-Tools-Features-and-Comparisons/index.md b/content/posts/2024/A-Deep-Dive-Into-Terraform-Static-Code-Analysis-Tools-Features-and-Comparisons/index.md index a456939f..21f4fd04 100644 --- a/content/posts/2024/A-Deep-Dive-Into-Terraform-Static-Code-Analysis-Tools-Features-and-Comparisons/index.md +++ b/content/posts/2024/A-Deep-Dive-Into-Terraform-Static-Code-Analysis-Tools-Features-and-Comparisons/index.md @@ -41,8 +41,6 @@ With a clear understanding of the necessary features in a static code analyzer, Let's take a closer look at some leading options! -{{< email-subscription >}} - ## Meet the Static Code Analyzers for Terraform Following on what makes a static code analyzer robust, let's dive into some open-source tools that exemplify these essential features. diff --git a/content/posts/2024/Mastering-AWS-API-Gateway-V2-HTTP-and-AWS-Lambda-with-Terraform/index.md b/content/posts/2024/Mastering-AWS-API-Gateway-V2-HTTP-and-AWS-Lambda-with-Terraform/index.md index fb647d0e..ce161510 100644 --- a/content/posts/2024/Mastering-AWS-API-Gateway-V2-HTTP-and-AWS-Lambda-with-Terraform/index.md +++ b/content/posts/2024/Mastering-AWS-API-Gateway-V2-HTTP-and-AWS-Lambda-with-Terraform/index.md @@ -40,8 +40,6 @@ Upon receiving a request, the API service forwards a payload to the Authorizer c The decision, Allow or Deny, is passed back to the API, and if allowed, the API service then forwards the original request to the back-end, which, in this case, is implemented by additional Lambda functions. Otherwise, the client gets a response with a 403 status code, and the original request is not passed to the back-end. -{{}} - ## Behind The Decision: Why Such a Setup? Choosing the right architectural setup is critical in balancing simplicity, cost-efficiency, and security. In this section, we uncover why integrating AWS HTTP API Gateway with Lambda Authorizer is a compelling choice, offering a streamlined approach without compromising security. diff --git a/content/posts/2025/delegate-for-growth-scaling-your-impact-through-others/index.md b/content/posts/2025/delegate-for-growth-scaling-your-impact-through-others/index.md index aa36219b..1df5d4ac 100644 --- a/content/posts/2025/delegate-for-growth-scaling-your-impact-through-others/index.md +++ b/content/posts/2025/delegate-for-growth-scaling-your-impact-through-others/index.md @@ -47,7 +47,7 @@ It rarely comes down to just one reason. Usually, it's a blend of motivations de - **The Strength of a Collaborative Culture:** In healthy teams, there's an inherent understanding that helping each other and sharing interesting work benefits everyone. Your offer reinforces this positive norm. Understanding these potential "Whys" is your key. Now, how do you translate that understanding into effectively approaching your peers? -{{< email-subscription >}} + ## Crafting the Opportunity: Making the Ask Compelling Knowing the potential motivators allows you to frame your request not as an ask for help but as presenting a valuable opportunity. diff --git a/layouts/_default/single.html b/layouts/_default/single.html deleted file mode 100644 index 24473d83..00000000 --- a/layouts/_default/single.html +++ /dev/null @@ -1,71 +0,0 @@ -{{- define "main" }} - -
-
- {{ partial "breadcrumbs.html" . }} -

- {{ .Title }} - {{- if .Draft }} - - - - - - {{- end }} -

- {{- if .Description }} -
- {{ .Description }} -
- {{- end }} - {{- if not (.Param "hideMeta") }} - - {{- end }} -
- {{- $isHidden := (.Param "cover.hiddenInSingle") | default (.Param "cover.hidden") | default false }} - {{- partial "cover.html" (dict "cxt" . "IsSingle" true "isHidden" $isHidden) }} - {{- if (.Param "ShowToc") }} - {{- partial "toc.html" . }} - {{- end }} - - {{- /* Hook: Beginning of post content */ -}} - {{- partial "hooks/post-content-begin.html" . -}} - - {{- if .Content }} -
- {{- if not (.Param "disableAnchoredHeadings") }} - {{- partial "anchored_headings.html" .Content -}} - {{- else }}{{ .Content }}{{ end }} -
- {{- end }} - - {{- /* Hook: End of post content */ -}} - {{- partial "hooks/post-content-end.html" . -}} - -
- {{- $tags := .Language.Params.Taxonomies.tag | default "tags" }} - - {{- if (.Param "ShowPostNavLinks") }} - {{- partial "post_nav_links.html" . }} - {{- end }} - {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }} - {{- partial "share_icons.html" . -}} - {{- end }} -
- - {{- if (.Param "comments") }} - {{- partial "comments.html" . }} - {{- end }} -
- -{{- end }}{{/* end main */}} diff --git a/layouts/partials/components/email-subscription-content.html b/layouts/partials/components/email-subscription-content.html deleted file mode 100644 index 348115d1..00000000 --- a/layouts/partials/components/email-subscription-content.html +++ /dev/null @@ -1,7 +0,0 @@ -
-

Subscribe to blog updates!

- -

No spam or promotions, or other crap. Just a gentle update when I publish a new post.

-
\ No newline at end of file diff --git a/layouts/partials/hooks/post-content-begin.html b/layouts/partials/hooks/post-content-begin.html deleted file mode 100644 index 97c41a88..00000000 --- a/layouts/partials/hooks/post-content-begin.html +++ /dev/null @@ -1,8 +0,0 @@ -{{- /* -Hook: Beginning of post content -This partial is included just before the post content. - -To use this hook: -1. Create layouts/partials/hooks/post-content-begin.html in your site -2. Add your custom content in that file -*/ -}} \ No newline at end of file diff --git a/layouts/partials/hooks/post-content-end.html b/layouts/partials/hooks/post-content-end.html deleted file mode 100644 index c80689ab..00000000 --- a/layouts/partials/hooks/post-content-end.html +++ /dev/null @@ -1,10 +0,0 @@ -{{- /* -Hook: End of post content -This partial is included right after the post content. - -To use this hook: -1. Create layouts/partials/hooks/post-content-end.html in your site -2. Add your custom content in that file -*/ -}} - -{{ partial "components/email-subscription-content.html" . }} \ No newline at end of file diff --git a/layouts/shortcodes/email-subscription.html b/layouts/shortcodes/email-subscription.html deleted file mode 100644 index e8f791e8..00000000 --- a/layouts/shortcodes/email-subscription.html +++ /dev/null @@ -1 +0,0 @@ -{{ partial "components/email-subscription-content.html" . }} \ No newline at end of file From 6572a7943416d510ab29d3c0b77527c6278cc9cb Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 13:31:13 +0100 Subject: [PATCH 4/9] Build multi-arch Docker image for native ARM64 support Use TARGETARCH in Dockerfile to download the correct Hugo binary per platform. Build for both linux/amd64 (CI) and linux/arm64 (macOS dev). CI workflow now uses docker/build-push-action with QEMU for cross-arch. Local builds are 2.6x faster (native ARM vs Rosetta emulation). Created with Claude Code under the supervision of Serhii Vasylenko --- .github/workflows/build-hugo-image.yaml | 18 ++++++++++++++++-- compose.yaml | 1 + hugo-runtime.dockerfile | 3 ++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-hugo-image.yaml b/.github/workflows/build-hugo-image.yaml index 1766f1f7..c5c11b7a 100644 --- a/.github/workflows/build-hugo-image.yaml +++ b/.github/workflows/build-hugo-image.yaml @@ -12,7 +12,21 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Authenticate to GitHub Container Registry run: echo "${{ github.token }}" | docker login ghcr.io -u $ --password-stdin - - name: Build and push Docker image - run: docker compose build --push \ No newline at end of file + - name: Read Hugo version + id: env + run: echo "HUGO_VERSION=$(grep HUGO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" + - name: Build and push multi-arch Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: hugo-runtime.dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: HUGO_VERSION=${{ steps.env.outputs.HUGO_VERSION }} + tags: ghcr.io/vasylenko/hugo-runtime:${{ steps.env.outputs.HUGO_VERSION }} diff --git a/compose.yaml b/compose.yaml index 92bf0d0c..f67cab08 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,7 @@ services: - "ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}" platforms: - "linux/amd64" + - "linux/arm64" expose: - 8080 ports: diff --git a/hugo-runtime.dockerfile b/hugo-runtime.dockerfile index 0261c463..a025ed15 100644 --- a/hugo-runtime.dockerfile +++ b/hugo-runtime.dockerfile @@ -1,8 +1,9 @@ FROM alpine:3.23 AS builder ARG HUGO_VERSION +ARG TARGETARCH WORKDIR /hugo RUN wget -q -c \ - "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.tar.gz" \ + "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-${TARGETARCH}.tar.gz" \ -O hugo.tar.gz RUN tar -xzf hugo.tar.gz From 0c84dcae7275751cfa63537ef9198c12eee1207d Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 13:53:53 +0100 Subject: [PATCH 5/9] Fix typo in action description and add MCP configuration for Chrome DevTools --- .github/actions/build-hugo-website/action.yaml | 2 +- .mcp.json | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .mcp.json diff --git a/.github/actions/build-hugo-website/action.yaml b/.github/actions/build-hugo-website/action.yaml index 7b4d6ed9..602a0011 100644 --- a/.github/actions/build-hugo-website/action.yaml +++ b/.github/actions/build-hugo-website/action.yaml @@ -1,5 +1,5 @@ name: 'Build Hugo Website' -description: 'This action build my hugo blod for preview or production env' +description: 'This action build my hugo blog for preview or production env' inputs: env: # id of input description: 'Environment name: preview or production' diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..c54fcd35 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "chrome-devtools": { + "command": "npx", + "args": ["-y", "chrome-devtools-mcp@latest"] + } + } +} \ No newline at end of file From 73d8c5ff03f6092b5c12c569a4a022cc67096772 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 14:00:33 +0100 Subject: [PATCH 6/9] Update Docker and CI configurations to include GO_VERSION and enhance documentation --- .env | 3 ++- .github/workflows/build-hugo-image.yaml | 10 +++++++--- CLAUDE.md | 13 +++++-------- compose.yaml | 1 + hugo-runtime.dockerfile | 7 +++++-- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.env b/.env index 929a9e80..695c98af 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -HUGO_VERSION=0.156.0 \ No newline at end of file +HUGO_VERSION=0.156.0 +GO_VERSION=1.25 \ No newline at end of file diff --git a/.github/workflows/build-hugo-image.yaml b/.github/workflows/build-hugo-image.yaml index c5c11b7a..3e666d3a 100644 --- a/.github/workflows/build-hugo-image.yaml +++ b/.github/workflows/build-hugo-image.yaml @@ -18,9 +18,11 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Authenticate to GitHub Container Registry run: echo "${{ github.token }}" | docker login ghcr.io -u $ --password-stdin - - name: Read Hugo version + - name: Read versions from .env id: env - run: echo "HUGO_VERSION=$(grep HUGO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" + run: | + echo "HUGO_VERSION=$(grep HUGO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" + echo "GO_VERSION=$(grep GO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" - name: Build and push multi-arch Docker image uses: docker/build-push-action@v6 with: @@ -28,5 +30,7 @@ jobs: file: hugo-runtime.dockerfile platforms: linux/amd64,linux/arm64 push: true - build-args: HUGO_VERSION=${{ steps.env.outputs.HUGO_VERSION }} + build-args: | + HUGO_VERSION=${{ steps.env.outputs.HUGO_VERSION }} + GO_VERSION=${{ steps.env.outputs.GO_VERSION }} tags: ghcr.io/vasylenko/hugo-runtime:${{ steps.env.outputs.HUGO_VERSION }} diff --git a/CLAUDE.md b/CLAUDE.md index d61ffcdf..b5c1ff21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,6 @@ Personal blog by Serhii Vasylenko. Shares experience and insights on Technical L - **PR previews**: Cloudflare Pages (`devdosvid-preview` project) - **Analytics**: Simple Analytics (privacy-first, no Google Analytics) - **Comments**: Giscus (GitHub Discussions-based) -- **Email subscriptions**: Beehiiv (embedded iframe) - **Fonts**: Albert Sans, self-hosted (no Google Fonts) - **Docker**: Hugo runtime image on `ghcr.io/vasylenko/hugo-runtime`, used in CI and optionally for local dev @@ -32,14 +31,12 @@ content/ # All site content (Markdown + YAML front matte search/index.md # Search page (requires JSON output format) layouts/ # Template overrides and custom components - _default/single.html # Full override of PaperMod's single post template _default/cv.html # Custom CV layout partials/ # PaperMod extension points and custom partials extend_head.html # Font faces, Twitter meta, Simple Analytics extend_footer.html # Ukrainian themed footer - hooks/post-content-end.html # Injects email subscription after post content comments.html # Giscus integration - shortcodes/ # 12 custom shortcodes (see SHORTCODES.md) + shortcodes/ # Custom shortcodes (see SHORTCODES.md) assets/css/extended/ # Custom CSS (PaperMod's user override directory) variables.css # CSS custom properties (design tokens, color palette) @@ -64,7 +61,7 @@ static/ # Favicons, fonts (woff2), social SVG icons **Hugo environment configs**: `development` is used locally (drafts enabled, no minification), `production` is used in CI (real base URL, minification, Simple Analytics injection) -**Docker image** (`hugo-runtime.dockerfile`): Two-stage build — Alpine downloads the Hugo extended binary, copies it into a `golang:1.22` image (Go is needed for Hugo modules). Published to `ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}`. Rebuilt automatically by CI when the Dockerfile, compose file, or `.env` changes. +**Docker image** (`hugo-runtime.dockerfile`): Two-stage build — Alpine downloads the Hugo extended binary, copies it into a Go image (Go is needed for Hugo modules). Published to `ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}`. Both Hugo and Go versions are pinned in `.env` and shared with CI. Rebuilt automatically by CI when the Dockerfile, compose file, or `.env` changes. ## Content Conventions @@ -92,8 +89,8 @@ static/ # Favicons, fonts (woff2), social SVG icons ### Templates (layouts) -- Extend PaperMod using its designated extension points (`extend_head.html`, `extend_footer.html`, `hooks/`) rather than copying entire templates -- When a full template override is unavoidable (like `single.html`), keep it as close to the original as possible +- Extend PaperMod using its designated extension points (`extend_head.html`, `extend_footer.html`) rather than copying entire templates +- Avoid full template overrides when possible — use PaperMod's extension points instead - Shortcodes use named parameters via `.Get "param"` and process inner content with `{{ .Inner | markdownify }}` ### CSS @@ -113,7 +110,7 @@ static/ # Favicons, fonts (woff2), social SVG icons ## Quirks ### PaperMod theme overrides are fragile -Custom layouts in `layouts/` override PaperMod templates. When the theme updates (via `go get`), overridden templates may break silently if PaperMod changes its internal structure. After theme updates, verify that `single.html`, `cover.html`, and all `extend_*.html` partials still work correctly. +Custom layouts in `layouts/` override PaperMod templates. When the theme updates (via `go get`), overridden templates may break silently if PaperMod changes its internal structure. After theme updates, verify that all `extend_*.html` partials and any custom layouts still work correctly. ### Docker build is required for CI parity The CI pipeline builds inside a Docker container (`ghcr.io/vasylenko/hugo-runtime`). Local `hugo` commands may produce different results if your local Hugo version differs. Use `docker compose up` to match CI behavior exactly. diff --git a/compose.yaml b/compose.yaml index f67cab08..7dbc782c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,6 +5,7 @@ services: dockerfile: hugo-runtime.dockerfile args: HUGO_VERSION: ${HUGO_VERSION} + GO_VERSION: ${GO_VERSION} tags: - "ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}" platforms: diff --git a/hugo-runtime.dockerfile b/hugo-runtime.dockerfile index a025ed15..26865ec4 100644 --- a/hugo-runtime.dockerfile +++ b/hugo-runtime.dockerfile @@ -1,3 +1,5 @@ +# Must be before the first FROM to be available in FROM instructions +ARG GO_VERSION=1.25 FROM alpine:3.23 AS builder ARG HUGO_VERSION ARG TARGETARCH @@ -7,12 +9,13 @@ RUN wget -q -c \ -O hugo.tar.gz RUN tar -xzf hugo.tar.gz -FROM golang:1.25 +FROM golang:${GO_VERSION} ARG HUGO_VERSION +ARG GO_VERSION COPY --from=builder /hugo/hugo / WORKDIR /site ENTRYPOINT ["/hugo"] LABEL org.opencontainers.image.source=https://github.com/vasylenko/devdosvid.blog -LABEL org.opencontainers.image.description="Official Hugo v${HUGO_VERSION} binary in Golang v1.25 Image to run or build a hugo website." +LABEL org.opencontainers.image.description="Official Hugo v${HUGO_VERSION} binary in Golang v${GO_VERSION} Image to run or build a hugo website." LABEL org.opencontainers.image.licenses=MIT From 0b089d06bcd7bfbda2f60d7be9f48dd0bcf727d4 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 14:06:11 +0100 Subject: [PATCH 7/9] Add comment to .env file for clarity on version definitions --- .env | 1 + 1 file changed, 1 insertion(+) diff --git a/.env b/.env index 695c98af..d7a249d3 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ +# Used by docker compose and is the main place to define the versions HUGO_VERSION=0.156.0 GO_VERSION=1.25 \ No newline at end of file From ed6a5ebb535890c3ff7969118d14d48633a62e23 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 14:20:05 +0100 Subject: [PATCH 8/9] Fix regex in version extraction from .env for accurate parsing --- .github/workflows/build-hugo-image.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-hugo-image.yaml b/.github/workflows/build-hugo-image.yaml index 3e666d3a..66421595 100644 --- a/.github/workflows/build-hugo-image.yaml +++ b/.github/workflows/build-hugo-image.yaml @@ -21,8 +21,8 @@ jobs: - name: Read versions from .env id: env run: | - echo "HUGO_VERSION=$(grep HUGO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" - echo "GO_VERSION=$(grep GO_VERSION .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" + echo "HUGO_VERSION=$(grep '^HUGO_VERSION=' .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" + echo "GO_VERSION=$(grep '^GO_VERSION=' .env | cut -d= -f2)" >> "$GITHUB_OUTPUT" - name: Build and push multi-arch Docker image uses: docker/build-push-action@v6 with: From e73ab5056eff61c92550324d15e18bb594cd77b0 Mon Sep 17 00:00:00 2001 From: Serhii Vasylenko Date: Thu, 19 Feb 2026 14:21:33 +0100 Subject: [PATCH 9/9] Add workflow file to trigger on changes to build-hugo-image.yaml --- .github/workflows/build-hugo-image.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-hugo-image.yaml b/.github/workflows/build-hugo-image.yaml index 66421595..ef9c4a0f 100644 --- a/.github/workflows/build-hugo-image.yaml +++ b/.github/workflows/build-hugo-image.yaml @@ -6,6 +6,7 @@ on: - 'hugo-runtime.dockerfile' - 'compose.yaml' - '.env' + - '.github/workflows/build-hugo-image.yaml' jobs: build: runs-on: ubuntu-latest