How I Set Up My Blog: A Developer's Journey to Efficiency and Simplicity
Requirements
When I decided to set up my own blog, my primary goals were low maintenance and minimal running costs, possibly even free. With these requirements in mind, I chose to use a static content generator, eliminating the need for hosting a database or performing software upgrades. Additionally, I wanted an easy way to write articles and nicely format code samples.
After some research, I opted to use Hugo. It ticked all the boxes for my needs: easy article writing in Markdown (so I wouldn’t have to worry about CSS or HTML), excellent syntax highlighting support, comprehensive and understandable documentation, and cross-platform compatibility (essential for a Mac user like me). Since articles are written in Markdown, I can use any editor that supports it.
I’m using the LoveIt theme for its minimalist look, which was exactly what I was after.
Folder Structure
I chose to use Page Bundles for my folder structure. This allows me to keep all the content for an article within its respective folder. I also decided to prefix folder names with the date and then the article title for organization and ease of finding. The “slug” in Front Matter is used to create user- and SEO-friendly URLs.
Hosting and Content Management
I use Azure DevOps daily, so it was natural for me to keep all the content there. I also use GitHub, mainly for larger code samples.
For hosting, I chose Azure Static Web Apps. Its free tier is perfect for hobby or personal projects, and deployment is a breeze. Since I’m using a .dev domain, I needed an SSL certificate, which Azure Static Web Apps provides for free. As of writing, it includes 100GB of bandwidth, which should suffice for now.
Build and Deployment Process
My experience with Azure DevOps CI/CD pipelines made the deployment process straightforward, involving a simple two-stage YAML pipeline with build and deployment stages.
The build process is quite simple:
- Install Hugo.
- Optimize Images.
- Build the site.
- Publish the produced artifacts.
Building the Container Image
I use (lighttpd) as a base for my image. It’s fast and lightweight, allowing me to test my site locally with a simple Docker Compose file.
version: "3"
services:
lighttpd:
image: sebp/lighttpd
volumes:
- ./public:/var/www/localhost/htdocs
ports:
- 8080:80
tty: true
I also build a multi-architecture image to run my site on my Raspberry Pi k8s cluster at home. I’ve considered hosting my site on this home cluster, and it might be a future consideration.
The Dockerfile requires few steps only
FROM sebp/lighttpd:latest
# Copy the files from the public folder to the htdocs folder
COPY public/ /var/www/localhost/htdocs/
EXPOSE 80
CMD ["start.sh"]
Why Optimize Images During Build?
Although Hugo has built-in image processing, I couldn’t make it work with the LoveIt theme. As I use the “windows-latest” image of Azure DevOps agent for the build stage, I can easily optimize images during this phase. Each agent comes with ImageMagick preinstalled, so I utilize it to optimize all images.
- task: PowerShell@2
displayName: 'Optimize images'
inputs:
targetType: 'inline'
script: |
# Define the directories and their maximum image sizes
$ImageDirectories = @{
"$(Build.SourcesDirectory)\content" = "1024x1024";
"$(Build.SourcesDirectory)\assets" = "256x256";
}
# Define the file formats to optimize
$FileFormats = @("*.jpg", "*.jpeg", "*.png")
# Loop through all specified directories and their sizes
foreach ($Directory in $ImageDirectories.Keys) {
$MaxSize = $ImageDirectories[$Directory]
# Check if the directory exists
if (Test-Path -Path $Directory) {
# Loop through all specified file formats in the current directory
foreach ($Format in $FileFormats) {
# Find images and process each one
Get-ChildItem -Path $Directory -Recurse -Filter $Format | ForEach-Object {
$ImagePath = $_.FullName
$TempPath = "$ImagePath.tmp"
# Copy the original image to a temporary file
Copy-Item -Path $ImagePath -Destination $TempPath
Write-Host "Optimizing $ImagePath to a maximum size of $MaxSize"
# ImageMagick command to resize and compress the temporary image
magick convert $TempPath -resize $MaxSize^> -quality 85 $TempPath
# Compare the file sizes
$OriginalSize = (Get-Item $ImagePath).Length
$OptimizedSize = (Get-Item $TempPath).Length
if ($OptimizedSize -lt $OriginalSize) {
# Replace original with optimized image if smaller
Move-Item -Path $TempPath -Destination $ImagePath -Force
} else {
# Delete the temporary file if it's not smaller
Remove-Item -Path $TempPath
}
}
}
} else {
Write-Host "Directory not found: $Directory"
}
}
pwsh: true
workingDirectory: '$(Build.SourcesDirectory)'
Azure Static Web App Configuration
By default, deploying to Azure Static Web Apps presents a challenge with handling 404 errors and URL formatting. If a user navigates to a non-existent URL, they encounter a default 404 page provided by Azure, which might not align with your site’s theme or structure. Additionally, the lack of proper configuration for trailing slashes can adversely impact SEO.
Configuring Azure Static Web Apps is crucial to address these issues. Detailed guidance can be found on the Configuration overview page. To ensure that your custom 404.html
file is utilized and trailing slashes are correctly handled, you should add a staticwebapp.config.json
file under the “static” folder. The content of this file should be structured as follows:
{
"trailingSlash": "auto",
"responseOverrides": {
"404": {
"rewrite": "/404.html"
}
}
}
Publishing The Produced Artifacts
The publishing process involves installing the SWA CLI, verifying the installation, and running the swa deploy
CLI command. The entire Azure DevOps YAML pipeline is available for reference.
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- feature/*
- master
- main
name: 0.$(Date:yy)$(DayOfYear).$(Rev:r)
variables:
- name: PackageVersion
value: '$(Build.BuildNumber)'
- group: MyBlog
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: BuildSite
pool:
vmImage: 'windows-latest'
displayName: 'Build Site'
steps:
- checkout: self
persistCredentials: true
clean: true
submodules: recursive
- task: Bash@3
displayName: 'Install Hugo'
inputs:
targetType: 'inline'
script: |
choco install hugo-extended
- task: PowerShell@2
displayName: 'Optimize images'
inputs:
targetType: 'inline'
script: |
# Define the directories and their maximum image sizes
$ImageDirectories = @{
"$(Build.SourcesDirectory)\content" = "1024x1024";
"$(Build.SourcesDirectory)\assets" = "256x256";
}
# Define the file formats to optimize
$FileFormats = @("*.jpg", "*.jpeg", "*.png")
# Loop through all specified directories and their sizes
foreach ($Directory in $ImageDirectories.Keys) {
$MaxSize = $ImageDirectories[$Directory]
# Check if the directory exists
if (Test-Path -Path $Directory) {
# Loop through all specified file formats in the current directory
foreach ($Format in $FileFormats) {
# Find images and process each one
Get-ChildItem -Path $Directory -Recurse -Filter $Format | ForEach-Object {
$ImagePath = $_.FullName
$TempPath = "$ImagePath.tmp"
# Copy the original image to a temporary file
Copy-Item -Path $ImagePath -Destination $TempPath
Write-Host "Optimizing $ImagePath to a maximum size of $MaxSize"
# ImageMagick command to resize and compress the temporary image
magick convert $TempPath -resize $MaxSize^> -quality 85 $TempPath
# Compare the file sizes
$OriginalSize = (Get-Item $ImagePath).Length
$OptimizedSize = (Get-Item $TempPath).Length
if ($OptimizedSize -lt $OriginalSize) {
# Replace original with optimized image if smaller
Move-Item -Path $TempPath -Destination $ImagePath -Force
} else {
# Delete the temporary file if it's not smaller
Remove-Item -Path $TempPath
}
}
}
} else {
Write-Host "Directory not found: $Directory"
}
}
pwsh: true
workingDirectory: '$(Build.SourcesDirectory)'
- task: Bash@3
displayName: 'Build site'
inputs:
targetType: 'inline'
script: |
hugo
dir
workingDirectory: '$(Build.SourcesDirectory)'
- task: CopyFiles@2
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
public/**
Dockerfile
TargetFolder: '$(Build.ArtifactStagingDirectory)'
CleanTargetFolder: true
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'public'
publishLocation: 'pipeline'
- job: BuildImage
dependsOn:
- BuildSite
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'public'
targetPath: '$(Build.ArtifactStagingDirectory)'
- task: Docker@2
inputs:
containerRegistry: 'DockerRegistry'
command: 'login'
- task: CmdLine@2
displayName: 'Set things'
inputs:
script: |
docker run --privileged --rm tonistiigi/binfmt --install arm64
docker run --privileged --rm tonistiigi/binfmt
docker buildx create --use
- task: CmdLine@2
displayName: 'Build Dockerfile'
inputs:
script: |
docker buildx build --platform 'linux/amd64,linux/arm64' \
-t $(CONTAINER_REGISTRY_NAME).azurecr.io/myblog:$(Build.BuildNumber) \
-t $(CONTAINER_REGISTRY_NAME).azurecr.io/myblog:latest \
-f ./Dockerfile \
--push \
.
workingDirectory: '$(Build.ArtifactStagingDirectory)'
- stage: Deploy
dependsOn:
- Build
jobs:
- deployment: DeployToStaticWebApp
displayName: Deploy to Azure Static Web App
pool:
vmImage: 'ubuntu-latest'
environment: MyBlog
strategy:
runOnce:
deploy:
steps:
- checkout: none
- download: none
- task: DownloadPipelineArtifact@2
inputs:
buildType: 'current'
artifactName: 'public'
targetPath: '$(Build.ArtifactStagingDirectory)'
- task: Bash@3
displayName: Install SWA CLI
inputs:
targetType: 'inline'
script: |
npm install -g @azure/static-web-apps-cli
- task: Bash@3
displayName: Validate SWA install
inputs:
targetType: 'inline'
script: |
swa --version
- task: Bash@3
displayName: Deploy
inputs:
workingDirectory: '$(Build.ArtifactStagingDirectory)'
targetType: 'inline'
script: |
swa deploy ./public --deployment-token $(SWA_CLI_DEPLOYMENT_TOKEN) --env Production
All variables come from the “MyBlog” variable group, where I keep the deployment token and the ACR name.
Cheers, Andrzej