What My Blog Architecture Looks Like
Many people are curious about how this blog is built.
Looks simple, but there’s a lot behind it. Not a big project, but from an engineering perspective, I did quite a few things. Bilingual architecture, automated build pipeline, PWA, Google SEO, and so on.
So I’ll write a series to document the current setup. What I’m using, why I chose it, how the code is written, what performance considerations were made.
Architecture
Markdown Content (bilingual)
↓
Hugo SSG (extended, v0.164)
↓
Custom Theme (forked cactus-dark, heavily modified)
↓
Gulp Pipeline
├── hugo --minify
└── Workbox generateSW
↓
Cloudflare Pages (GitHub Actions CI)
Three layers: content layer, build layer, deployment layer. No database, no backend, all static files.
Why Hugo
I’m a frontend engineer, I like using tools from the frontend ecosystem. Hugo is written in Go, and when you have a couple hundred articles, the build speed is simply fast. That’s the core reason.
When build is fast, the mental barrier to update the blog becomes low. It’s a positive feedback loop.
Go templates are pleasant to use. Variable scope, pipeline syntax, partial system, they cover what you need. One thing though: the difference between {{ range }} and {{ with }} might trip you up at first.
Why Gulp
Hugo handles static generation well, but I needed to integrate service worker generation and some post-processing. Gulp is the glue. No need for a complex build system, a few tasks chained together get the job done.
The whole gulpfile.js is about 50 lines:
const gulp = require("gulp");
const shell = require('gulp-shell');
const workbox = require('workbox-build');
gulp.task('build', () => gulp.src('./').pipe(shell(['hugo --buildFuture --minify'])));
gulp.task('generate-service-worker', () => {
return workbox.generateSW({
globDirectory: './public',
globPatterns: ['**/*.{woff2,woff,js,css,png.jpg}'],
globIgnores: ['sw.js'],
swDest: `./public/sw.js`,
clientsClaim: true,
skipWaiting: true,
runtimeCaching: [
{ urlPattern: /.*\.js/, handler: 'NetworkFirst' },
{ urlPattern: /.*\.css/, handler: 'StaleWhileRevalidate' },
{ urlPattern: /.*\.(?:png|jpg|jpeg|svg|gif)/, handler: 'CacheFirst',
options: { cacheName: 'images', expiration: { maxEntries: 50 } } },
{ urlPattern: /.*\.html/, handler: 'NetworkFirst' }
]
})
.then(() => console.info('SW generated.'))
.catch(error => console.warn('SW generation failed: ' + error));
});
gulp.task("default", gulp.series('build', 'generate-service-worker'));
Two tasks, serial execution. Build first, then generate SW.
Why Cloudflare Pages
Zero configuration for static sites, global CDN, free plan covers the needs. GitHub Actions integration is straightforward, push to master triggers build and deploy.
- name: Deploy to Cloudflare Pages
run: npx wrangler pages deploy public --project-name=time-friend
The whole CI file is about 35 lines. Setup Hugo, install deps, build, deploy.
One thing: the Cloudflare Pages project name can’t be changed after creation. If you want to rename it, you have to delete and recreate the project. A bit annoying.
Performance Optimizations
Build Time
Hugo builds in seconds regardless of content volume. The --minify flag handles HTML-level compression, no additional processing needed.
Page Load
Static HTML with no server-side rendering overhead. Theme CSS is about 20KB. No render-blocking JavaScript by default; scripts are loaded in js.html partial at the bottom of the page.
Caching Strategy
Service Worker handles runtime caching with different strategies per resource type. Details on why each strategy is chosen are covered in a separate article: How I Made My Blog Work Offline.
After the first visit, most resources are served from cache. Subsequent navigation feels instant.
Google SEO
Every article gets JSON-LD Article structured data injected at build time. In head.html:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "{{ .Title }}",
"datePublished": "{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}",
"dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}",
"author": { "@type": "Person", "name": "{{ .Site.Params.author }}" },
"description": "{{ .Description }}",
"url": "{{ .Permalink }}"
{{ with .Params.tags }}
,"keywords": "{{ delimit . ", " }}"
{{ end }}
}
</script>
Google Search Console verified via DNS record. Sitemap auto-generated by Hugo. Canonical URL on every page. That’s it, other search engines not configured.
Series Articles
All articles in one place, no fixed order, easy to add more:
- Overview (this one) — Architecture, tech choices, rationale
- Homepage Typing Effect — Typed.js, quotes.json, implementation
- i18n Internationalization — Content + UI internationalization, translationKey, module mounts
- Theme Customization — Which templates were overridden, features added
- Build Pipeline — Gulp tasks, CI/CD
- Google SEO — Structured data, meta tags, sitemap, verification
- Deployment — Cloudflare Pages setup, environment management, domain config
- Giscus Comment System — Integration, GitHub Discussions, full flow
- Page Analytics — Busuanzi + Google Analytics, dual system
- PWA & Service Worker — What is SW, caching strategies, manifest.json