I’ve just added an “Edit in VSCode” button to my blog that appears when running the local server. Click it and the post opens directly in VSCode at the right location.
This implementation is based on Zoltan Toma’s excellent guide - you should read his article first as he’s funnier than me and explains everything thoroughly.
Why not use Blowfish’s built-in edit button?#
Blowfish has an edit button feature, but it’s designed for GitHub/GitLab links. Using the vscode:// protocol triggers Hugo’s security and replaces the URL with #ZgotmplZ. That string means ‘unsafe, I won’t show it to you’.
Implementation#
The implementation uses Hugo’s environment-specific configs and the vscode://file URL scheme.
Environment Configs#
Created config/development/hugo.toml to enable the button in dev mode:
[params.editPost]
enabled = true
URL = "vscode://file"
workingDir = "/Users/yourname/dev/yourwebsite"
And config/production/hugo.toml to disable it in production:
[params.editPost]
enabled = false
Edit Button Partial#
Created layouts/partials/edit_post.html with a conditional button that constructs the VSCode URL:
{{ if site.Params.editPost.enabled }}
{{ with .File }}
{{ $filePath := path.Join site.Params.editPost.workingDir .Path }}
{{ $vscodeURL := printf "%s%s" site.Params.editPost.URL $filePath | safeURL }}
<a href="{{ $vscodeURL }}" class="edit-post-link">
<!-- SVG icon and "Edit in VSCode" text -->
</a>
{{ end }}
{{ end }}
Link Renderer Override#
Added layouts/_default/_markup/render-link.html to support the vscode:// protocol :
{{- if or (strings.HasPrefix .Destination "http:")
(strings.HasPrefix .Destination "https:")
(strings.HasPrefix .Destination "vscode:") }}
target="_blank"
{{- end }}
Template Integration#
I copied themes/blowfish/layouts/_default/single.html to layouts/_default/single.html and added one line ({{ partial "edit_post.html" . }}) in the article header section, leaving everything else identical.
This works because Blowfish is a git submodule, so editing theme files directly would lose changes on updates. Hugo checks layouts/ in my project before themes/blowfish/layouts/, which means I can pull theme updates without losing customisations.
Running different versions#
Development server with drafts and edit buttons
hugo server --environment development -D
Production server for testing (no edit buttons)
hugo server --environment production
Build setup for live on Cloudflare
hugo --environment production --minify
This ensures the edit button stays hidden on the live site.
Photo gallery setup reminder#
Since I’m documenting site improvements, here’s a quick reminder for future me about handling images and galleries.
The gallery pattern I should use#
{{< gallery >}}
<img src="images/photo.jpg" class="grid-w33" alt="Description" data-zoom-src="images/photo-scaled.jpg" />
{{< /gallery >}}
What each part does:
{{< gallery >}}- Hugo shortcode that wraps images in a grid layout with medium-zoom enabledsrc="images/photo.jpg"- The display/thumbnail version (usually 1024px wide)data-zoom-src="images/photo-scaled.jpg"- Full-size image that opens when clicked (2048px or original)class="grid-w33"- Makes a 3-column grid layoutalt="Description"- Descriptive text for accessibility
Why this works for my site#
My site uses Hugo’s page bundles pattern where each post has its own directory (content/posts/YYYY/slug/index.md) with images stored in an images/ subdirectory. Blowfish includes the medium-zoom library, so clicking any image (except those with class="nozoom") opens a full-size overlay. This is particularly useful for my WordPress migration, where old posts used clickable image wrappers like [](hard-coded-url) which break Hugo’s render-image hook - the gallery shortcode is the modern replacement for that pattern.
