<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><docs>https://blogs.law.harvard.edu/tech/rss</docs><title>Posts on tkalp.dev</title><link>https://tkalp.dev/post/</link><description>Recent content in Posts on tkalp.dev</description><image><title>Posts on tkalp.dev</title><link>https://tkalp.dev/post/</link><url>https://source.unsplash.com/collection/983219/2000x1322</url></image><ttl>1440</ttl><generator>After Dark 10.1.0 (Hugo 0.163.1)</generator><language>en-US</language><copyright>Copyright &copy; blacksamsays. Licensed under CC-BY-ND-4.0.</copyright><lastBuildDate>Thu, 06 Aug 2026 11:09:49 UT</lastBuildDate><atom:link href="https://tkalp.dev/post/index.xml" rel="self" type="application/rss+xml"/><item><title>A Cost-Effective Personal Blog with Hugo, GitHub, and Cloudflare Pages</title><link>https://tkalp.dev/post/cost-effective-personal-blog-hugo-github-cloudflare-pages/</link><pubDate>Thu, 06 Aug 2026 00:00:00 UT</pubDate><guid>https://tkalp.dev/post/cost-effective-personal-blog-hugo-github-cloudflare-pages/</guid><description>A personal blog does not need a database, a virtual server, or another monthly subscription. If the site is mostly articles, images, and a few static pages, a static-site stack can be simpler, faster, and almost free to run.
This setup uses Hugo to build the site, GitHub to store its source, and Cloudflare Pages to publish it. The only recurring cost is usually the domain renewal.</description><category domain="https://tkalp.dev/categories/tech">Tech</category><content:encoded><![CDATA[A personal blog does not need a database, a virtual server, or another monthly subscription. If the site is mostly articles, images, and a few static pages, a static-site stack can be simpler, faster, and almost free to run.
This setup uses Hugo to build the site, GitHub to store its source, and Cloudflare Pages to publish it. The only recurring cost is usually the domain renewal.
The short version: write a post, push it to GitHub, and Cloudflare automatically publishes it. Why a static blog? Traditional publishing platforms often bundle together a web server, database, admin panel, plugins, updates, and backups. That makes sense for a dynamic site, but it is a lot of machinery for a personal blog.
Hugo takes a different approach. It converts Markdown files into plain HTML, CSS, JavaScript, and images. The deployed site does not need a database or application server. There is less to maintain, and static files are easy to cache around the world.
The result is pleasantly boring:
no server operating system to patch no database to back up no WordPress plugins to babysit no idle virtual machine running all month very little attack surface fast page loads almost everywhere The stack Hugo Hugo is the site generator. Posts live as Markdown files in a Git repository, usually in a structure like this:
content/ post/ first-post/ index.md images/ cover.jpg A basic site configuration might include:
baseurl = &#34;https://tkalp.dev/&#34; languageCode = &#34;en-US&#34; title = &#34;tkalp.dev&#34; theme = [&#34;your-theme&#34;] enableRobotsTXT = true The baseurl value matters more than it first appears. Hugo uses it for canonical URLs, feeds, sitemaps, and other generated links. It should match the final HTTPS domain, including the trailing slash.
GitHub GitHub is the source of truth. It stores the Hugo configuration, theme, posts, and images while giving every change a useful history.
A normal publishing workflow is simply:
git add . git commit -m &#34;post: add a new article&#34; git push origin main There is no need to upload the generated public directory manually. Cloudflare can build it directly from the source repository.
Cloudflare Pages Cloudflare Pages connects to the GitHub repository. Every push to the production branch starts a build and, if it succeeds, publishes the result to Cloudflare&rsquo;s global network.
For this Hugo project, the important build settings are:
Production branch: main Build command: hugo --gc --minify --baseURL https://tkalp.dev/ Build output directory: public Environment variable: HUGO_VERSION=0.163.1 Pinning the Hugo version is a small but worthwhile detail. It prevents an unexpected generator update from changing the build or breaking an older theme. Cloudflare&rsquo;s official Hugo deployment guide covers the repository connection and build settings.
Connect the custom domain Cloudflare Pages provides a temporary project.pages.dev address, but a personal domain makes the site easier to remember and gives it a permanent identity.
In the Pages project:
Open Custom domains. Add the apex domain, such as tkalp.dev. Add www.tkalp.dev if it should also work. Wait for the DNS record and TLS certificate to become active. When the domain is already managed by Cloudflare, most of this is automatic. DNS and SSL do not require separate paid products.
Choose one canonical hostname, then redirect the other public versions to it:
www.tkalp.dev/* → https://tkalp.dev/:splat project.pages.dev/* → https://tkalp.dev/:splat Cloudflare&rsquo;s Bulk Redirects can preserve the path suffix and query string. A request for:
https://www.tkalp.dev/post/hello/?source=rss then lands on:
https://tkalp.dev/post/hello/?source=rss That avoids duplicate versions of the same page and keeps shared links predictable.
What publishing looks like Once Git integration is enabled, publishing becomes almost uneventful:
Write or edit a Markdown post. Preview it locally with hugo server. Commit the change. Push to main. Cloudflare installs the pinned Hugo version. It runs the build command. The contents of public are deployed automatically. A useful local check is:
hugo --gc --minify If that command works locally, the Cloudflare build will usually behave the same way. The Pages dashboard keeps build logs and previous deployments, so a broken release is easy to diagnose or roll back.
The actual cost For a purely static personal blog, the running cost can be close to zero.
Component Typical cost Hugo Free and open source GitHub repository Free for this use case Cloudflare Pages static hosting Free DNS and SSL Free Bulk Redirects Included in the Free plan Custom domain Annual registration and renewal Cloudflare&rsquo;s Free plan currently includes up to 500 builds per month, far more than a personal blog normally needs. Purely static asset requests are free and unlimited; Pages Functions instead count toward Workers quotas.
There is also no surprise compute bill because this setup does not need Pages Functions, Workers, R2, image transformations, or a database. Those products can be useful later, but they are not required for a straightforward blog.
A few SEO basics Static generation handles much of the plumbing, but it is worth checking the output before calling the site finished:
every page has a self-referencing canonical URL robots.txt allows normal crawling sitemap.xml contains the final domain RSS links use the final domain internal navigation uses the canonical hostname the www and pages.dev versions redirect to the canonical domain After launch, add the domain to Google Search Console and submit:
https://tkalp.dev/sitemap.xml Search Console is useful for spotting indexing errors, checking search queries, and confirming that Google can crawl the site.
Small gotchas worth avoiding Building with the wrong Hugo version Themes sometimes rely on a particular Hugo release. Pin HUGO_VERSION in Cloudflare rather than relying on whichever version happens to be the default.
Forgetting the production base URL A site can look fine while quietly generating canonical links and feeds for the wrong hostname. Set the domain in config.toml and include it in the Cloudflare build command.
Publishing generated files to Git The public and resources directories are build output. In most setups they should be ignored by Git and rebuilt on every deployment.
Adding dynamic features too early Comments, forms, server-side search, and analytics can introduce third-party services or dynamic compute. Add them only when they solve a real problem. A client-side search index is often enough for a small blog.
Forgetting the non-canonical domains The apex domain, www, and default pages.dev URL may all serve the same content unless redirects are configured. Pick one public address and redirect everything else to it.
Final thoughts This stack is a good fit for anyone who wants to own their content without turning a blog into a small infrastructure project.
Hugo keeps the content portable. GitHub provides version history. Cloudflare Pages handles builds, TLS, caching, and global delivery. The workflow stays simple: Markdown in, static files out.
Most importantly, it stays cheap. There is no server waiting for traffic and no monthly hosting plan to justify. For a straightforward personal blog, the domain renewal can genuinely be the only recurring bill.
Sources Hugo documentation Cloudflare Pages: deploy a Hugo site Cloudflare Pages limits Cloudflare Pages Functions routing and static asset requests Cloudflare Bulk Redirects ]]></content:encoded></item><item><title>Affordable Off-Site Photo Backup with Synology and Backblaze B2</title><link>https://tkalp.dev/post/affordable-off-site-photo-backup-synology-backblaze-b2/</link><pubDate>Wed, 05 Aug 2026 11:00:00 UT</pubDate><guid>https://tkalp.dev/post/affordable-off-site-photo-backup-synology-backblaze-b2/</guid><description>Photos and family videos are some of the few digital files you simply cannot recreate. A NAS gives you a convenient local copy, but it cannot protect you from fire, theft, major hardware failure, or a mistake that affects the entire system.
This guide builds an affordable, encrypted off-site backup around Synology Photos, Hyper Backup, and Backblaze B2.</description><category domain="https://tkalp.dev/categories/tech">Tech</category><content:encoded>Photos and family videos are some of the few digital files you simply cannot recreate. A NAS gives you a convenient local copy, but it cannot protect you from fire, theft, major hardware failure, or a mistake that affects the entire system.
This guide builds an affordable, encrypted off-site backup around Synology Photos, Hyper Backup, and Backblaze B2.
The backup plan The setup uses:
Synology Photos to collect and organise photos Hyper Backup for versioning, compression, and deduplication Backblaze B2 as affordable S3-compatible cloud storage Client-side encryption before anything leaves the NAS The complete data flow looks like this:
Phones and computers → Synology Photos → Hyper Backup → Backblaze B2 → a replacement NAS or Hyper Backup Explorer
Hyper Backup runs every day and keeps multiple restore points with Smart Recycle. Because the backups are incremental and deduplicated, unchanged files are not uploaded in full for every new version.
In one real-world setup with roughly 300 GB of data, the monthly bill stayed at only a few dollars, including local tax. Your cost will depend on the amount of data you store and Backblaze&amp;rsquo;s current pricing.
Configure Hyper Backup Install Hyper Backup from Synology Package Center. Create a new Folders and Packages backup task. Choose an S3-compatible destination. Enter the regional S3 endpoint provided by Backblaze. Select only the data you actually care about: Synology Photos as an application important personal folders documents any static archive worth keeping off-site Enable: compression client-side encryption a daily schedule Smart Recycle with a sensible version limit Hyper Backup downloads the encryption key locally. Store it in a password manager and keep a second copy on an offline medium. If both the password and the key are lost, the backup cannot be recovered. The bucket setting that can quietly increase storage use Hyper Backup already manages version history inside its .hbk repository. If the Backblaze bucket remains on the default Keep all versions setting, Backblaze may also retain older versions of internal objects that Hyper Backup changes or deletes.
For this type of repository, set the bucket lifecycle rule to:
Keep only the last version
Hyper Backup will continue to manage your restore points, while the storage provider avoids keeping a second, indefinite layer of object versions in the background.
Backup retention and provider lifecycle rules are separate controls. Configure both deliberately.
Real growth or runaway versioning? A gradually rising cloud bill does not automatically mean something is wrong. If Source Disk Usage is growing at roughly the same pace, the extra storage probably represents real data—usually new photos and large videos.
Before reducing retention or deleting backups, compare growth on the source and destination. If they rise together, you are probably paying for new files rather than uncontrolled versioning.
Fix a partially completed backup Hyper Backup may still create new versions while reporting Partially completed if the task references a source folder that no longer exists.
To fix it:
Open the most recent backup logs. Identify the missing source folder. Remove it from the backup task. Run a manual backup. Confirm that the result changes to Success. The detailed logs are much more useful than the general status message when a task only completes partially.
Test a real restore A backup is not real until you have restored something from it. The most useful test happens inside Backup Explorer:
Select a recent backup version. Find a photo from a personal Synology Photos space. Download it. Confirm that the file opens normally. That small test verifies several things at once:
personal photos are included the repository is readable decryption works a genuine recovery path exists A green status icon is reassuring. A successfully restored file is evidence.
Recover after losing the entire NAS If the NAS fails completely, the preferred recovery path is:
Set up a replacement Synology running the same or a newer Hyper Backup version. Choose Restore from existing repositories. Connect to the existing S3-compatible bucket. Select the existing .hbk repository. Enter the encryption password or provide the key. Choose a backup version. Restore the photo folders and the Synology Photos application. Relink the repository so future backups can continue. You can also download the complete repository to a computer and open it with Hyper Backup Explorer. For several hundred gigabytes this is slower, but it gives you a recovery option without buying a replacement NAS immediately.
Lessons worth keeping RAID is not a backup. Your NAS and cloud storage should be able to fail independently. Check both the application backup and the actual files in Backup Explorer. Provider lifecycle rules and backup retention are separate layers. Keep the encryption key outside the infrastructure it protects. Investigate warnings even when new backup versions are still being created. One real restore test is worth more than a hundred green status indicators. Add a periodic offline USB copy for fuller 3-2-1 protection. Final thoughts The result is simple: photos remain easy to access on the Synology, Hyper Backup keeps their history, and Backblaze provides the remote copy.
The important thing is not merely knowing that &amp;ldquo;the backup is running.&amp;rdquo; It is knowing where the data lives, keeping the keys safe, and proving in advance that your files can come back.
Sources Synology Hyper Backup Restoring backup data with Hyper Backup Hyper Backup data backup settings and client-side encryption Backblaze B2 pricing Backblaze lifecycle rules </content:encoded></item><item><title>Return to Monkey Island (quick review)</title><link>https://tkalp.dev/post/rtmi/</link><pubDate>Sat, 08 Oct 2022 15:09:55 UT</pubDate><guid>https://tkalp.dev/post/rtmi/</guid><description>Intro After almost 30 years, Ron Gilbert, Dave, and the team behind the original Monkey Island series return to our screens. Was the wait worth it?</description><category domain="https://tkalp.dev/categories/games">Games</category><content:encoded><![CDATA[Intro After almost 30 years, Ron Gilbert, Dave, and the team behind the original Monkey Island series return to our screens. Was the wait worth it?
I have this strange relationship with LucasArts adventures of the late 80s/early 90s. Is it the childhood memories? Is it the magic of interacting with a computer for the very first time? Is it the amazing stories these games have to tell? One thing is for sure, the mark of the Monkey Island series on me is permanent! The mark of the golden era of adventure games. Spoilers ahead!!! The Story You start right after the end of Monkey Island 2. You are Guybrush&rsquo;s son, Boybrush, and you are with your friend, Chukie. So the riddle of the MI2 ending is answered right here in the opening sequence. It was just the fantasy of two kids playing pirates all along! This puts an end to one of the community theories about the MI2 ending being a voodoo curse. Check the hillarius fact how the creators pick Guybrush as the name of our Monkey Island protagonist Guybrush Threepwood Visuals As an avid old-school adventure gamer, I did expect a more retro look for the comeback, something similar to Ron&rsquo;s Thimbleweed Park graphics. But I get it. Years have passed, hardware has evolved, and habits have changed. Not everybody is playing on their PC&rsquo;s keyboard and mouse; People nowdays use tablets, phones, consoles, etc. So I guess the game should scale visually and practically nicely on all platforms. Besides, the creative cue is quite unique, and I felt right at home. So no complaints here; the visuals match the atmosphere and the aesthetics of our notorious mighty pirate&rsquo;s saga with noteworthy evolution brushes. Audio Well, just by the first seconds in game, the quality of the music and sound effects stands out. The iconing tunes along with the opening theme is there, melticusly executed by Michael Land, Peter McConnell, and Clint Bajakian. Every scene in the game is so beatifully surrounding your ears making the experience unforgetable. Like, for example the Scumm Bar tune, go there and reach the right end of the bar. Hear the music transitioning to a rock version of the tune! Absolutely epic! Of course it wouldn&rsquo;t be a solid Monkey Island sequel without the spectacular voice of Dominic Armato. All in all voices are superb and the sound quality in the game raises the bar even further. Gameplay Post in progress ]]></content:encoded></item><item><title>Bash on Mac</title><link>https://tkalp.dev/post/bash-on-mac/</link><pubDate>Sun, 13 Mar 2022 00:00:00 UT</pubDate><guid>https://tkalp.dev/post/bash-on-mac/</guid><description>The problem Since macOS Catalina and later Apple has changed the default shell terminal to zsh. Most of your scripts based on bash will have incompatibility issues with zsh. You can switch to bash but the version that comes with macOS is pretty old. Let’s change that.</description><category domain="https://tkalp.dev/categories/tech">Tech</category><content:encoded><![CDATA[The problem Since macOS Catalina and later Apple has changed the default shell terminal to zsh. Most of your scripts based on bash will have incompatibility issues with zsh. You can switch to bash but the version that comes with macOS is pretty old. Let’s change that.
Assumptions You are on a mac device running macOS 10.15 and later. Install brew /bin/bash -c &#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)&#34; Install the latest Bash and make it your default login shell # install latest bash brew install bash # install newer bash completions brew install bash-completion@2 # Add /usr/local/bin/bash to the list of allowed login shells echo /usr/local/bin/bash | sudo tee -a /etc/shells # Change the default shell for your Mac terminal chsh -s /usr/local/bin/bash # Change the login shell for your user sudo chpass -s /usr/local/bin/bash your_username Append to your ~/.bash_profile # Git functions git_branch() { git branch 2&gt;/dev/null | sed -e &#39;/^[^*]/d&#39; -e &#39;s/* \(.*\)/ (\1)/&#39; } # Terminal prompt PS1=&#34;\[\e[0;93m\]\u\[\e[m\]@&#34; # username PS1+=&#34;\[\e[0;92m\]\h\[\e[m\]&#34; # hostname PS1+=&#34; &#34; # space PS1+=&#34;\[\e[0;95m\]\W\[\e[m\]&#34; # current directory PS1+=&#34;\[\e[0;92m\]\$(git_branch)\[\e[m\]&#34; # current branch PS1+=&#34; &#34; # space PS1+=&#34;$ &#34; # end prompt export PS1 export CLICOLOR=1 export LSCOLORS=GxFxCxDxBxegedabagaced export PATH=&#34;$HOME/bin:/usr/local/bin:/usr/local/opt/python/libexec/bin:$HOME/.local/bin:$PATH&#34; # Bash v5 completions [[ -r &#34;/usr/local/etc/profile.d/bash_completion.sh&#34; ]] &amp;&amp; . &#34;/usr/local/etc/profile.d/bash_completion.sh&#34; # Get rid of Apple&#39;s Bash deprecation warning export BASH_SILENCE_DEPRECATION_WARNING=1 ]]></content:encoded></item><item><title>Wildcard Let's Encrypt certs on Kubernetes with Traefik</title><link>https://tkalp.dev/post/how-to-ssl-with-letsencrypt-and-kubernetes-traefik/</link><pubDate>Fri, 13 Mar 2020 00:00:00 UT</pubDate><guid>https://tkalp.dev/post/how-to-ssl-with-letsencrypt-and-kubernetes-traefik/</guid><description>Ingredients For this recipe we will need:
A VPS instance or other server preferably with 1GB ram AWS Route 53 DNS zone A kubernetes cluster. In our case k3s cert-manager</description><category domain="https://tkalp.dev/categories/tech">Tech</category><content:encoded><![CDATA[Ingredients For this recipe we will need:
A VPS instance or other server preferably with 1GB ram AWS Route 53 DNS zone A kubernetes cluster. In our case k3s cert-manager Heads-up: Guide below works as a checklist to achieve what is stated and assumes familiarity with Kubernetes and AWS. Solution is based on Cert-Manager Steps First get a server &#x1f604; Scaleway, Amazon Lightsail, DigitalOcean and Vultr are among the popular and affordable choices. You can get a decent VPS for as less as maybe 6$/mo (at the time of this writing) that will easily get the job done.
Choose your favorite distro. This exercise has been done on Ubuntu 18.04 but other distros will also work fine. Just pick your favorite one and install your k3s server.
Head over to k3s.io and follow the recommended method to install a single node k3s directly on your host.
bash curl -sfL https://get.k3s.io | sh - # Check for Ready node, takes maybe 30 seconds k3s kubectl get node Since k3s comes with traefik ingress controller baked-in, you can go ahead and create an ingress for your domain right away. This example from the documentation will get you started. traefik UI service &amp; ingress yaml apiVersion: v1 kind: Service metadata: name: traefik-web-ui namespace: kube-system spec: selector: k8s-app: traefik-ingress-lb ports: - name: web port: 80 targetPort: 8080 --- apiVersion: extensions/v1beta1 kind: Ingress metadata: name: traefik-web-ui namespace: kube-system spec: rules: - host: traefik-ui.minikube http: paths: - path: / backend: serviceName: traefik-web-ui servicePort: web Create your DNS record sets to match your domain and static IP on AWS Route 53. While you are here note your AWS_HOSTED_ZONE_ID
In the IAM console of AWS create a user with administrator access to your AWS account.
Note the AWS_ACCESS_KEY_ID &amp; the AWS_SECRET_ACCESS_KEY for the account you created in the previous step.
Deploy Cert-manager on your cluster
Configure DNS01 challenge (recommended)
You should end up with an Issuer yaml file like this. Apply and enjoy &#x1f389;
cert-manager issuer yaml apiVersion: cert-manager.io/v1 kind: Issuer metadata: ... spec: acme: ... solvers: - selector: dnsZones: - &#39;example.com&#39; - dns01: # Valid values are None and Follow cnameStrategy: Follow route53: region: eu-central-1 accessKeyID: &lt;Access ID for less-privileged.example.org here&gt; hostedZoneID: &lt;Zone ID for less-privileged.example.org here&gt; secretAccessKeySecretRef: ... ]]></content:encoded></item></channel></rss>