[{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/blog/","section":"Blog","summary":"","title":"Blog","type":"blog"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/","section":"cp1251 / shiryavsky","summary":"","title":"cp1251 / shiryavsky","type":"page"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/firebase/","section":"Tags","summary":"","title":"Firebase","type":"tags"},{"content":" A static site with a blog, a photo blog and an about page, built in one long day. Here\u0026rsquo;s the whole recipe, simplified, including the parts that didn\u0026rsquo;t work the first time. The stack # Hugo (extended edition) generates the site. Blowfish is the theme, added as a git submodule and never edited directly. Everything custom lives in the site\u0026rsquo;s own layouts/, assets/ and config/. PhotoSwipe shows photos full-screen. Firebase (Firestore with anonymous sign-in) stores view and like counts. Google Analytics, loaded only after the visitor agrees. 1. Project skeleton # hugo new site example.com \u0026amp;\u0026amp; cd example.com git init git submodule add -b main https://github.com/nunocoracao/blowfish.git themes/blowfish # Blowfish wants its config split into files: mkdir -p config/_default \u0026amp;\u0026amp; cp themes/blowfish/config/_default/*.toml config/_default/ rm hugo.toml Three sections, each post a page bundle (a folder with index.md and its images):\ncontent/ ├── blog/ text posts ├── photos/ photo posts, shown as cards └── about/ about page, with a downloadable CV The menu is three entries in menus.en.toml, and mainSections = [\u0026quot;blog\u0026quot;, \u0026quot;photos\u0026quot;] puts both on the homepage.\n2. A photo gallery # Blowfish has a gallery, but I wanted justified rows (every photo in a row has the same height) and a proper full-screen viewer. So the site has its own photoswipe shortcode. A photo post just needs a gallery/ folder and one line:\n{{\u0026lt; photoswipe \u0026gt;}} The shortcode lists the images in the folder and makes two copies of each:\n{{ range sort (.Page.Resources.Match \u0026#34;gallery/*\u0026#34;) \u0026#34;Name\u0026#34; }} {{ $full := .Process \u0026#34;fit 2560x2560 webp q85\u0026#34; }} {{ $thumb := .Process \u0026#34;resize x480 webp q80\u0026#34; }} \u0026lt;a href=\u0026#34;{{ $full.RelPermalink }}\u0026#34; data-pswp-width=\u0026#34;{{ $full.Width }}\u0026#34; data-pswp-height=\u0026#34;{{ $full.Height }}\u0026#34;\u0026gt; \u0026lt;img src=\u0026#34;{{ $thumb.RelPermalink }}\u0026#34; class=\u0026#34;nozoom\u0026#34; loading=\u0026#34;lazy\u0026#34; alt=\u0026#34;\u0026#34;\u0026gt; \u0026lt;/a\u0026gt; {{ end }} The justified rows are pure CSS: each item\u0026rsquo;s flex-grow is its aspect ratio, so a whole row scales together.\nPhotoSwipe itself is vendored into assets/ (no CDN), and its script and styles load only on pages that use the shortcode.\nBlowfish\u0026rsquo;s own click-to-zoom attaches to every image without a nozoom class. Without it, one click opened two viewers on top of each other. 3. Captions from the photo itself # Hugo can read a photo\u0026rsquo;s metadata with .Meta, so captions come from what my photo editor (Capture One) already wrote:\nTitle and description: IPTC ObjectName and Caption-Abstract. Place: a keyword like Place: Lisbon. Camera line: model, lens, focal length, aperture, shutter, ISO, date. Lisbon · X100V · 23mm · f/5.6 · 1/250s · ISO 160 · 12 May 2026 EXIF numbers like aperture come back as fractions (14/5); Hugo\u0026rsquo;s float turns them into 2.8. Any caption can still be overridden in front matter.\n4. Keeping GPS out of the published site # My photos carry GPS coordinates, and a static site happily publishes them unless you stop it. Four layers make sure nothing identifying leaks:\nEvery photo is re-encoded. Hugo\u0026rsquo;s processed images carry no metadata at all.\nOriginals aren\u0026rsquo;t published. By default Hugo copies every file in a post\u0026rsquo;s folder. One setting stops that:\n[[cascade]] [cascade.build] publishResources = false The social preview image and the RSS feed used the originals too. Two small template overrides make them use re-encoded copies.\nA build script strips whatever is left (the theme still publishes a few originals), and fails the build if anything identifying remains:\nhugo --gc --minify --cleanDestinationDir exiftool -r -overwrite_original -all= --ICC_Profile:all \\ -tagsFromFile @ -Orientation public/ The script keeps the colour profile and rotation, and caches its results by file hash, so unchanged photos are never processed twice. Hugo already caches its own image processing by content.\nexiftool -all= -tagsFromFile @ -ICC_Profile looks like it keeps the colour profile, but silently deletes it. Exclude it from the wipe instead: --ICC_Profile:all. 5. Views and likes # Blowfish supports view and like counters out of the box: set the Firebase config in params.toml and turn on showViews / showLikes. Visitors are signed in anonymously and the counters live in Firestore.\nThe Firebase config is public by design, so the security rules are the only protection. Mine allow only the counters, changed by exactly one per write:\nmatch /views/{id} { allow read: if true; allow create: if request.auth != null \u0026amp;\u0026amp; request.resource.data.views == 1; allow update: if request.auth != null \u0026amp;\u0026amp; request.resource.data.keys().hasOnly([\u0026#39;views\u0026#39;]) \u0026amp;\u0026amp; request.resource.data.views == resource.data.views + 1; } 6. Likes on every photo # Each photo has its own heart, on the thumbnail and in the full-screen view. A few design decisions:\nOne document per gallery, not per photo, holding a count for each photo. Opening a gallery costs one database read, however many photos it has. Rules can\u0026rsquo;t see which photo changed, so each write also names the photo it changes, and the rule checks that exactly that count moved by ±1. A photo post\u0026rsquo;s like count is the sum of its photo likes. Each photo like also moves the post\u0026rsquo;s own counter in the same atomic batch, so the theme shows the right total everywhere with no extra code. Photo posts don\u0026rsquo;t get a separate \u0026ldquo;like the post\u0026rdquo; button. const batch = writeBatch(db); batch.set(photosDoc, { likes: { [photo]: increment(1) }, last: photo }, { merge: true }); batch.set(postDoc, { likes: increment(1) }, { merge: true }); await batch.commit(); 7. Analytics, after consent # Google Analytics is added by setting its ID in hugo.toml, but it doesn\u0026rsquo;t load until the visitor clicks Accept in a small banner:\nNothing is requested from Google before that, and no cookies are set. The choice is remembered; Cookie settings in the footer reopens the banner, and declining later deletes the cookies. Browsers sending Global Privacy Control count as Decline. In development the banner works, but Accept only logs to the console, so local browsing never reaches the real statistics. 8. Polish # Reading width. Article text is 18px and 80 characters wide, set with two CSS variables in assets/css/custom.css. A pixel-art favicon and logo (an emerald). Pixel art only stays crisp at whole multiples of its grid, so the logo is drawn natively at 36px rather than shrunk from 48. An about page built from my CV, with the PDF downloadable. Lessons # false in front matter often doesn\u0026rsquo;t override a site-wide true in Blowfish: Hugo\u0026rsquo;s default treats false as \u0026ldquo;not set\u0026rdquo;. Turn features on per section with cascade instead of off per page. Keep theme overrides small and documented. Every copied template is something to re-check after a theme update. Check privacy in the output, not the templates. Scanning the built site with exiftool found leaks that reading the code would have missed. The repository has a README.md for me and an AGENTS.md for coding agents, so the next round of changes starts from what\u0026rsquo;s written down, not from memory.\n","date":"4 October 2026","externalUrl":null,"permalink":"/blog/how-this-site-is-built/","section":"Blog","summary":"The recipe behind this site: Hugo and Blowfish, a photo gallery that keeps GPS data private, per-photo likes on Firebase, and analytics behind a consent banner.","title":"How This Site Is Built","type":"blog"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/photography/","section":"Tags","summary":"","title":"Photography","type":"tags"},{"content":"","date":"4 October 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"5 January 2026","externalUrl":null,"permalink":"/photos/","section":"Photos","summary":"","title":"Photos","type":"photos"},{"content":"","date":"5 January 2026","externalUrl":null,"permalink":"/tags/stockholm/","section":"Tags","summary":"","title":"Stockholm","type":"tags"},{"content":"Photos taken during my trip to Stockholm.\n","date":"5 January 2026","externalUrl":null,"permalink":"/photos/stockholm-vibes/","section":"Photos","summary":"A serene winter scene unfolds with snow-covered trees and a frozen river under a clear blue sky.","title":"Stockholm Vibes","type":"photos"},{"content":"","date":"5 January 2026","externalUrl":null,"permalink":"/tags/winter/","section":"Tags","summary":"","title":"Winter","type":"tags"},{"content":" Hi, I\u0026rsquo;m Alexey: an engineering leader and hands-on Go engineer, working on high-load distributed systems for the last 19 years. Most of my career has been in marketing automation and messaging. In 2015 I built the Altcraft Marketing Platform from scratch and led its technology as CTO until 2025. It\u0026rsquo;s an omnichannel customer data platform that runs on-premise at large enterprises and as multi-tenant SaaS, with over 450 million customer profiles under management.\nWhat I enjoy most is architecture and performance engineering of systems under heavy load, and building the engineering teams that run them.\nThis site is where I write about software and post photos from walks and trips: see the blog and the photo blog.\nDownload CV (PDF) What I do now # Since 2025 I\u0026rsquo;ve been a fractional CTO and technical consultant for Altcraft:\nArchitecture, performance and AI-engineering advice for the platform. Load testing and capacity planning for enterprise on-premise deployments. Leading Altcraft Cloud SMTP, a sending service built on KumoMTA. Introducing AI-assisted engineering: LLM agents connected to the issue tracker, GitLab and browser automation via MCP. Experience # Chief Technology Officer, Altcraft · 2017–2025\nOwned architecture, technical strategy, roadmap and releases as the platform grew to 450M+ customer profiles. Led Development, QA and Operations: up to 35 people under three department heads, and covered product management too. Set up a Project/Product Management Office and handed it over to its own head. Delivered on-premise installations for enterprise clients in aviation, banking, insurance, telecom, e-commerce and media. Lead Developer \u0026amp; Team Lead, Altcraft · 2015–2017\nBuilt the marketing-automation platform from scratch (Go backend, JavaScript frontend); co-inventor on a patent covering it. Earlier · 2006–2015\nSoftware engineer for marketing-automation and email deliverability tooling at GMBDirect (2007–2015). Electronics and software prototyping for escape rooms; UI design and web development. Skills # Backend Go (10+ years), JavaScript / Node.js, Python, Bash; gRPC, HTTP/REST, SMTP/MTA, SMPP Data \u0026amp; messaging MongoDB, ClickHouse, PostgreSQL, Redis, RabbitMQ; storage tuning and query optimisation Performance \u0026amp; ops Load testing, bottleneck analysis, capacity planning; Linux, Docker, GitLab CI, Prometheus, Grafana, Zabbix, ELK AI engineering Agentic workflows: prompt and context engineering, MCP tool use, RAG, evaluation and agent loops Leadership Three departments, up to 35 people; strategy and roadmap; product management; standards, code review, team metrics Languages # Russian (native) · English (professional working proficiency) · German (B1)\nContact # Email: cp1251@pm.me LinkedIn: linkedin.com/in/cp1251 Telegram: t.me/cp1251 ","externalUrl":null,"permalink":"/about/","section":"cp1251 / shiryavsky","summary":"","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]