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
4 changes: 3 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
HUGO_VERSION=0.143.1
# Used by docker compose and is the main place to define the versions
HUGO_VERSION=0.156.0
GO_VERSION=1.25
2 changes: 1 addition & 1 deletion .github/actions/build-hugo-website/action.yaml
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
25 changes: 22 additions & 3 deletions .github/workflows/build-hugo-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,32 @@ on:
- 'hugo-runtime.dockerfile'
- 'compose.yaml'
- '.env'
- '.github/workflows/build-hugo-image.yaml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
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
- 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"
- 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 }}
GO_VERSION=${{ steps.env.outputs.GO_VERSION }}
tags: ghcr.io/vasylenko/hugo-runtime:${{ steps.env.outputs.HUGO_VERSION }}
2 changes: 1 addition & 1 deletion .github/workflows/deploy-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/deploy-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand All @@ -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: |
Expand Down
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}
22 changes: 9 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -58,13 +55,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 <slug>` — scaffolds a page bundle from the `post-bundle` archetype
**New blog post**: `./newpost.sh <slug>` — 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 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

Expand All @@ -86,15 +83,14 @@ 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

### 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
Expand All @@ -114,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.
Expand Down
23 changes: 0 additions & 23 deletions assets/css/extended/email-subscription-form.css

This file was deleted.

2 changes: 1 addition & 1 deletion assets/css/extended/minimalistic-blog-items-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
}
.post-entry figure.entry-cover img {
width: 300px;
= }
}

.post-entry div.entry-content {
grid-area: content;
Expand Down
2 changes: 2 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ services:
dockerfile: hugo-runtime.dockerfile
args:
HUGO_VERSION: ${HUGO_VERSION}
GO_VERSION: ${GO_VERSION}
tags:
- "ghcr.io/vasylenko/hugo-runtime:${HUGO_VERSION}"
platforms:
- "linux/amd64"
- "linux/arm64"
expose:
- 8080
ports:
Expand Down
1 change: 0 additions & 1 deletion config/development/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ buildFuture: true
buildExpired: true
params:
env: development
EmailSubscribeForm: true
minify:
disableCSS: false
disableHTML: false
Expand Down
4 changes: 1 addition & 3 deletions config/production/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ minify:
tdewolff:
html:
keepComments: false
keepConditionalComments: true
keepSpecialComments: true
keepDefaultAttrVals: true
keepDocumentTags: true
keepEndTags: true
Expand All @@ -17,5 +17,3 @@ minify:

params:
env: production # to enable google analytics, opengraph, twitter-cards and schema.
heapanalyticsid: 2300872157
EmailSubscribeForm: true
2 changes: 0 additions & 2 deletions content/about/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ hideSummary: true
cover.hidden: true
ShowBreadCrumbs: false
title: About me — Serhii Vasylenko
kind: page
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/).
Expand Down
1 change: 0 additions & 1 deletion content/cv/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

{{<email-subscription>}}

## Terraform modules
You already write modules even if you think you don’t.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

{{<email-subscription>}}

## 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

{{<email-subscription>}}

## What are security headers, and why it matters
Security Headers are one of the web security pillars.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

{{<email-subscription>}}

## 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

{{<email-subscription>}}

## Conditional resource arguments (attributes) setting
{{<figure src="conditional-resource-argument.png" width="400">}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

{{<email-subscription>}}

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

{{<email-subscription>}}

## 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Loading