<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Alex&#039;s Notebook</title>
	<atom:link href="https://blog.alexseifert.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.alexseifert.com</link>
	<description>Technology and Horror</description>
	<lastBuildDate>Mon, 20 Jul 2026 20:37:34 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2024/02/cropped-favicon.png?fit=32%2C32&#038;ssl=1</url>
	<title>Alex&#039;s Notebook</title>
	<link>https://blog.alexseifert.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">3693140</site>	<item>
		<title>A Few New Pages</title>
		<link>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/</link>
					<comments>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 20 Jul 2026 20:37:34 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[IndieWeb]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12585</guid>

					<description><![CDATA[Not too long ago, I ran into an idea that is gaining traction in the blog/personal website space called slash pages. I decided to create a few of them myself.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Not too long ago, I ran into an idea that is gaining traction in the blog/personal website space called <a href="https://slashpages.net" data-type="link" data-id="https://slashpages.net" target="_blank" rel="noreferrer noopener">slash pages</a>. Essentially, it is a list of standardized URLs for pages that have certain functions and/or contain specific information about the person or website. The idea behind it is that people will automatically know where to find what information. How well that works certainly depends on how familiar the visitor is with slash pages, so milage will vary.</p>



<p class="wp-block-paragraph">Nonetheless, I thought I&#8217;d jump on the bandwagon and create a few of them myself. Most of them I don&#8217;t really see a need to bother with, so I won&#8217;t, but I did create a few I thought would be interesting and/or helpful.</p>



<p class="wp-block-paragraph">Here are the new pages I just created:</p>



<ul class="wp-block-list">
<li><a href="/ai">/ai</a> &#8211; How I use AI.</li>



<li><a href="/blogroll">/blogroll</a> &#8211; A few of the blogs I&#8217;ve subscribed to.</li>



<li><a href="/changelog">/changelog</a> &#8211; Where I document changes made to this website.</li>



<li><a href="/colophon">/colophon</a> &#8211; The technologies that power this website.</li>



<li><a href="/sitemap">/sitemap</a> &#8211; A directory of all the pages on this website.</li>
</ul>



<p class="wp-block-paragraph">I may or may not add more as I go, but that&#8217;s all I felt like doing tonight. The list above is an incomplete list of pages on this website, but you can see all of them on the <a href="/sitemap" data-type="link" data-id="/sitemap" target="_blank" rel="noreferrer noopener">Sitemap page</a> which WordPress automatically generates and keep up to date.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/20/a-few-new-pages/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12585</post-id>	</item>
		<item>
		<title>Conversations I’ve Had with AI</title>
		<link>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/</link>
					<comments>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sat, 18 Jul 2026 20:48:22 +0000</pubDate>
				<category><![CDATA[News]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[AI]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12538</guid>

					<description><![CDATA[Lately, I’ve been having a lot of interesting conversations with LLMs about programming and software architecture. I've decided to start posting some of them.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a man talking to a robot over tea" class="wp-image-12537" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/07/img_7371.png?w=1408&amp;ssl=1 1408w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a man talking to a robot over tea</figcaption></figure>
</div>


<p class="wp-block-paragraph">Lately, I’ve been having a lot of very interesting conversations with LLMs about a wide range of topics. However, most of them have pertained to technical topics and I thought it would be interesting to post some of them on my blog. I figure there are probably plenty of other people who might be interested in the questions I ask and what AI has to say about it, so why not post them? If nothing else, then at least it’s a way of preserving them for my future self.</p>



<p class="wp-block-paragraph">Most of my recent discussions have been about the pros and cons of various frameworks and programming languages for specific use cases. I’ve also asked about how best to architect specific scenarios such as the massively complex single-page frontend applications used by AWS, Google Cloud Platform and Microsoft Azure. I’ve thoroughly enjoyed the conversations as they’ve been extremely informative and thought-provoking, even if I wouldn’t always necessarily take its advice verbatim.</p>



<p class="wp-block-paragraph">Most of these conversations I’ve had have been with Gemini as I’ve found it to give me the most thought-provoking answers. I’ve also had a few with GitHub Copilot using the Claude Sonnet and Codex models, but due to the recent extreme increase in price, I’ve started saving my tokens for actual programming tasks rather than casual conversations.</p>



<p class="wp-block-paragraph">One of the aspects I enjoy most about these talks with AI is that I find the occasional gem that really helps me out. For example, a couple of days ago, I was discussing the longevity of PHP projects in a classic LAMP stack and mentioned that I was using Apache with mod_php — something I’ve done for nearly three decades. It suggested switching to php-fpm instead in order to boost my websites’ speed. Of course I’d heard of php-fpm, but had never actually used it personally. Changing my server config was a simple task that actually tangibly boosted the speed of all the websites I host on it, including this blog. It was a simple thing I wouldn’t have thought of that had a major impact.</p>



<p class="wp-block-paragraph">In any case though, I’m going to go through some of my recent conversations and copy them into posts on this blog. I’ll start with an introduction to explain a little bit of the background of each conversation and conclude them with my thoughts about it and whether it led to any direct action on my part either at work or in my personal projects. I may edit out parts of the discussions that aren’t strictly relevant as I tend to go off on tangents before eventually telling AI to get back to the original question, but I’ll try to keep them as close to the original conversation as possible.</p>



<p class="wp-block-paragraph">I hope you’ll enjoy them as much as I have and that maybe they’ll even prove inspirational.</p>



<p class="wp-block-paragraph"><em>Just a side note about my blogs: Other than some images (like the one above), I don’t post any AI-generated content that isn’t explicitly marked as such. Blogging is a passion of mine and I feel no need to fluff my post count with articles not written by me. You can read more about how I use AI on the <a href="/ai" data-type="link" data-id="/ai" target="_blank" rel="noreferrer noopener">AI page</a>.</em></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/18/conversations-ive-had-with-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12538</post-id>	</item>
		<item>
		<title>The Bride of Ashbyrn House by Amy Cross</title>
		<link>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/</link>
					<comments>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sun, 12 Jul 2026 12:20:59 +0000</pubDate>
				<category><![CDATA[Books]]></category>
		<category><![CDATA[Featured]]></category>
		<category><![CDATA[Ghosts]]></category>
		<category><![CDATA[Horror]]></category>
		<category><![CDATA[Amy Cross]]></category>
		<category><![CDATA[Book Reviews]]></category>
		<category><![CDATA[Spoiler]]></category>
		<guid isPermaLink="false">https://haunting.alexseifert.com/?p=572</guid>

					<description><![CDATA[Before reading The Bride of Ashbyrn House, I had never read anything by Amy Cross. Not only did the novel impress me, but it quickly became one of my all-time favorites.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1024%2C768&#038;ssl=1" alt="The Bride of Ashbyrn House by Amy Cross" class="wp-image-577" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/02/img_6941.jpg?w=2000&amp;ssl=1 2000w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">The Bride of Ashbyrn House by Amy Cross</figcaption></figure>
</div>


<p class="wp-block-paragraph">Before reading <em>The Bride of Ashbyrn House</em>, I had never read anything by <a href="https://blog.alexseifert.com/tag/amy-cross/" data-type="post_tag" data-id="904" target="_blank" rel="noreferrer noopener">Amy Cross</a>. Not only did the novel impress me, but it quickly became one of my all-time favorites. That&#8217;s an impressive feat for a new-to-me author to achieve after only a single book. As such, I knew I had to write about it next.</p>



<p class="wp-block-paragraph">The story had everything I love about ghost stories: an old haunted manor, a vengeful ghost, an intriguing story and a sense of thrill that kept the pages turning. This time though, they were digital pages as I read it on my Kindle rather than as a paperback. As of this writing, it isn&#8217;t available in paperback form.</p>



<p class="wp-block-paragraph">Here is the description of the book from&nbsp;<a href="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" target="_blank" rel="noreferrer noopener">Goodreads</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph">“I have waited so long for your return.”</p>



<p class="wp-block-paragraph">In the English countryside, miles from the nearest town, there stands an old stone house. Nobody has set foot in the house for years. Nobody has dared. For it is said that even though the lady of the house is long dead, a face can sometimes be seen at one of the windows. A pale, dead face that waits patiently behind a silk wedding veil.Seeking an escape from his life in London, Owen Stone purchases Ashbyrn House without waiting to find out about its history. As far as Owen is concerned, ghosts aren&#8217;t real and his only company in the house will be the thin-legged spiders that lurk on the walls. Even after he moves in, and after he starts hearing strange noises in the night, Owen insists that Ashbyrn House can&#8217;t possibly be haunted. But Owen knows nothing about the ghostly figure that is said to haunt the house. Or about the mysterious church bells that ring out across the lawn at night. Or about the terrible fate that befell the house&#8217;s previous inhabitants when they dared defy the bride. Even as Owen starts to understand the horrific truth about Ashbyrn House&#8217;s past, he might be too late to escape the clutches of the presence that watches his every move. The Bride of Ashbyrn House is a ghost story about a man who believes the past can&#8217;t hurt him, and about a woman whose search for a husband has survived even her own tragic death.</p>
<cite><a href="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" data-type="link" data-id="https://goodreads.com/book/show/32714766-the-bride-of-ashbyrn-house" target="_blank" rel="noreferrer noopener">Goodreads</a></cite></blockquote>



<p class="wp-block-paragraph">Since I&#8217;m going to freely discuss the story, <strong><em>there will be spoilers!</em></strong> Continue at your own risk!</p>



<p class="wp-block-paragraph">Recently, it feels like there has been an onslaught of horror stories that have to do with dead brides. I seem to see books and movies about them all over the place, but of all of them I have read or seen, <em>The Bride of Ashbyrn House</em> stands out as the best. The story primarily takes place at Ashbyrn House in Cornwall, England. It alternates between the events of 1859 when an upperclass woman named Katinka Ashbyrn is due to marry her fiancé, and the present when a man named Owen buys the allegedly haunted property. The further the story moves along, the clearer the bridge between the two time periods becomes.</p>



<h2 class="wp-block-heading">Katinka</h2>



<p class="wp-block-paragraph">The story begins from Katinka’s perspective. She is watching from the house as her husband comes up to the gates outside, sees her, then turns around and leaves, abandoning her. It immediately leaves the reader with a feeling of unease, a sense of sympathy for Katinka, and many questions. Fortunately, they are answered throughout the story, however, some of them not until the last few pages. The reader has no idea at this point that the opening scene takes place in the present.</p>



<p class="wp-block-paragraph">Most of the story from Katinka’s perspective takes place in 1859, shortly before her planned wedding. She wants it to be absolutely perfect and nothing will stop her from getting what she wants. First and foremost, however, she is to be the picture-perfect centerpiece of it all. In her vanity, she commissions a portrait of herself which arrives shortly before the wedding. For the next several days, much of her time is occupied with sitting and admiring herself in the painting. Eventually, she convinces herself that she appears much more attractive in the picture than in real life which she won’t tolerate. Thus, she begins a strict regiment of self-mutilation to ensure she is skinnier and, in her mind, prettier — the perfect centerpiece for the wedding.</p>



<p class="wp-block-paragraph">At this point in the story, it’s clear that she is a selfish, highly toxic person who doesn’t quite have all of her ducks in a row. Aside from what she does to herself, she practically worships her dead father while belittling and berating her mother and her younger sister, Pippa. Her fiancé is also subjected to torrents of verbal abuse which culminates in her catching him cheating on her in the pantry with Pippa. For that, Katinka murders her sister coldheartedly without so much as batting an eye.</p>



<p class="wp-block-paragraph">Come the day of the wedding, all of the guests have assembled at Ashbyrn house. To make her grand entrance, Katinka has arranged to ride in a coach. However, as the coach rumbles down the rough driveway, tragedy strikes. Katinka, who has used a healthy dose of paraffin — a highly flammable substance — on her skin to make herself appear perfect, is jerked out of her seat and her face comes into contact with the candle lighting the interior of the coach. Instantly catching fire, she stumbles out of the coach screaming for help, but no one comes to her rescue. Instead, they just watch as she jumps into a pond where her weighted dress pulls her down to the bottom where she ultimately drowns. As she is dying, she can see the guests assembling around the pond watching her, but not bothering to help her. She dies unwed.</p>



<h2 class="wp-block-heading">Owen</h2>



<p class="wp-block-paragraph">Fast forward to the present. A man named Owen buys the abandoned Ashbyrn House to escape the hustle of London and focus on his writing. He is told of the story of the ghost bride, but dismisses it as pure fancy. Naturally, he comes to regret that. The ghost of Katinka seems almost cautious at first, but the longer Owen occupies the house, the more confident she becomes and eventually starts playing the “perfect bride.” While he is concentrated on his writing, she brings him food and water and takes care of him in every way. However, it’s as though Owen is in a trance during that time and has no idea about what is actually happen. Not only does he stay blissfully unaware of Katinka’s existence, but he can quite literally sit and write for days without any clue as to how much time has passed.</p>



<p class="wp-block-paragraph">Owen’s ex-fiancé, Vanessa, who is worried about him ignoring all correspondence, eventually travels from London to check on him. Owen still doesn’t realize what has been going on and refuses to leave the house or acknowledge that anything might be off.&nbsp; The more Vanessa tries to convince him, though, the more aggressive Katinka becomes in defending Owen, who, for better or worse, she essentially considers her husband at this point.</p>



<p class="wp-block-paragraph">Eventually, Owen snaps out of it and figures out what is actually happening. He tries to escape Ashbyrn House with Vanessa, but Katinka has other plans and refuses to let them leave the grounds. Owen and Vanessa come to the conclusion that if Owen officially marries Katinka in the ruins of the Ashbyrn’s private church where she was originally supposed to be married, that she might at least let Vanessa go. Katinka seems convinced by the idea and so Vanessa officiates the wedding between Owen and the ghost of Katinka.</p>



<p class="wp-block-paragraph">Shortly thereafter, Vanessa escapes, but Owen stays. Vanessa, however, eventually comes back for Owen. This time, Katinka isn’t having it though and kills her. Owen then manages to escape with Vanessa’s body, never to set foot in Ashbyrn House again.</p>



<p class="wp-block-paragraph">The story ends with a mirrored version of the opening scene. Owen is looking through the locked gate of Ashbyrn House decades later after having served a long prison sentence for the murder of Vanessa. He sees Katinka watching him from the house and decides to leave again without actually entering the property. Despite not unlocking the gate, Owen already had the key to it in his hand which leads the reader to wonder what exactly his intensions were since he very well knows what waits for him beyond the gate.</p>



<h2 class="wp-block-heading">My Conclusion</h2>



<p class="wp-block-paragraph">Owen’s and Katinka’s perspectives alternate as both stories simultaneously lead to their individual, yet related, climaxes. It was an incredibly exciting journey that really kept me turning the digital pages wanting to see how it continued. As it progressed, it was hard to determine which character deserved my sympathy the most. In some ways, despite her somewhat evil nature, I couldn’t help but feel sorry for Katinka. In other ways, it was Vanessa who was the most innocent, yet arguably paid the highest price of them all. For some reason, I could never really feel much sympathy for Owen until the end when the reader finds out he had to spend decades in prison for a murder he didn’t commit.</p>



<p class="wp-block-paragraph">I found the ending to be unnerving in that no one got their comeuppance or was vindicated. Instead, it was the most realistic outcome possible for such a wild, supernatural story. There was no satisfaction or relief for any of the characters whatsoever which made its realism all the more disturbing. What I found excellent about the ending, however, was how it tied in directly to the beginning, just from Owen’s perspective rather than Katinka’s. It also clarified who the anonymous husband actually was and why exactly he chose to just turn around and leave again after seeing her. It was one of the best wrap-ups I’ve seen in a story.</p>



<p class="wp-block-paragraph"><em>The Bride of Ashbyrn House</em> instantly made me a fan of Amy Cross. I&#8217;ve since read two more of her books and I can say with certainty that she has firmly established herself as a staple in my personal library.</p>



<p class="wp-block-paragraph">You can find more about Amy Cross on <a href="https://www.amycross.com/" target="_blank" rel="noreferrer noopener">her website</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/07/12/the-bride-of-ashbyrn-house-by-amy-cross/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">572</post-id>	</item>
		<item>
		<title>Building a Web App in Swift Using Only SwiftNIO</title>
		<link>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/</link>
					<comments>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 29 Jun 2026 12:18:27 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[Featured]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12418</guid>

					<description><![CDATA[I’ve been experimenting a lot with Swift for web applications using frameworks like Vapor, but I got curious as to how I could create a low-level, framework-less web application using only SwiftNIO. This is the result.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a man presenting &quot;Web App Using SwiftNIO&quot; to an audience" class="wp-image-12422" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Gemini_Generated_Image_unxjd1unxjd1unxj.png?w=1408&amp;ssl=1 1408w" sizes="(max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a man presenting &#8220;Web App Using SwiftNIO&#8221; to an audience</figcaption></figure>
</div>


<p class="wp-block-paragraph">Recently, I’ve been experimenting a lot more with Swift as a language for web applications. I’ve primarily been focusing my efforts on the web framework <a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a> which is the most complete and popular web framework for Swift and is used by the likes of some very large projects such as the <a href="https://swiftpackageindex.com" target="_blank" rel="noreferrer noopener">Swift Package Index</a>.</p>



<p class="wp-block-paragraph">The framework is based on <a href="https://github.com/apple/swift-nio" target="_blank" rel="noreferrer noopener">SwiftNIO</a> which is Apple’s open-source “cross-platform asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers &amp; clients.” I also recently read somewhere that Apple itself uses SwiftNIO to power its newer web services as it is low-level and low-latency — both critical aspects considering the sheer enormity of the number of requests that Apple’s web services have to process. Whether or not that is true is something I can’t confirm, but it sounds plausible considering it is an Apple-developed framework that does exactly what a high-performance web service would need.</p>



<p class="wp-block-paragraph">So, naturally, I asked myself what it would take to make my own basic web app using SwiftNIO without any other dependencies beyond the three others that SwiftNIO itself relies on. As it turns out, it doesn’t actually take as much as I thought. Since I wanted to keep this experiment simple, I focused on basic functionality rather than implementing a fully featured web framework. If I wanted that, I would just go with Vapor.</p>



<p class="wp-block-paragraph">Before we start, I want to mention that I pushed the entire experiment to <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub</a> with thoroughly commented code so that you can see what I did and/or play around with it yourself. There will be several links to various parts of the code on GitHub through the post.</p>



<h2 class="wp-block-heading"><strong>Setting Up the Server</strong></h2>



<p class="wp-block-paragraph">The first, and arguably most critical, feature any web application needs is a server. In this case, one that uses SwiftNIO for network communication. In order to accomplish that, I created two classes: <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L166" target="_blank" rel="noreferrer noopener">HTTPServer</a> and <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L28" target="_blank" rel="noreferrer noopener">HTTPHandler</a>. As the names might suggest, the first sets up the server and network infrastructure while the latter handles each individual connection.</p>



<p class="wp-block-paragraph">The <code>HTTPServer</code> class includes a method called <code>start()</code> which uses SwiftNIO’s <a href="https://swiftpackageindex.com/apple/swift-nio/main/documentation/nioposix/serverbootstrap" target="_blank" rel="noreferrer noopener">ServerBootstrap</a> to bind a TCP port and configure how connections are handled:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">public func start() async throws {
    let router = self.router

    let bootstrap = ServerBootstrap(group: group)
        // How many pending connections the OS kernel should queue before the
        // application has a chance to accept them.
        .serverChannelOption(ChannelOptions.backlog, value: 256)
        // Allow the port to be reused immediately after the process restarts,
        // without waiting for the OS TIME_WAIT period to expire.
        .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
        // For each accepted connection, configure the NIO pipeline:
        //   1. Built-in HTTP/1.1 encoder + decoder
        //   2. Our custom HTTPHandler for routing
        .childChannelInitializer { channel in
            channel.pipeline.configureHTTPServerPipeline().flatMap {
                channel.pipeline.addHandler(HTTPHandler(router: router))
            }
        }

    let channel = try await bootstrap.bind(host: host, port: port).get()
    print(&quot;Server running on http://\(host):\(port)&quot;)

    // Suspend here until the channel is closed. In normal operation this never
    // returns — the server runs until the process is killed.
    try await channel.closeFuture.get()
}
</code></pre>
</div>



<p class="wp-block-paragraph">That’s all that the <code>HTTPServer</code> class actually does. The rest happens in the <code>HTTPHandler</code> class which, as you may have noticed is added as a handler to the channel pipeline.</p>



<p class="wp-block-paragraph">For each incoming request, a new <code>HTTPHandler</code> instance is created and is responsible for four different tasks:</p>



<ol class="wp-block-list">
<li>Receiving request parts</li>



<li>Bridging to Swift concurrency via Task</li>



<li>Writing the response</li>



<li>Closing the connection</li>
</ol>



<p class="wp-block-paragraph">This is the lifecycle of a single request:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">Client connects
        ↓
ServerBootstrap accepts → creates HTTPHandler
        ↓
SwiftNIO fires channelRead(.head)  → store head
SwiftNIO fires channelRead(.body)  → accumulate bytes  (0 or more times)
SwiftNIO fires channelRead(.end)   → dispatch to router via Task
        ↓
Router matches route → calls handler → returns HTTPResponse
        ↓
eventLoop.execute → write response head + body + end
        ↓
Connection closed
</code></pre>
</div>



<h3 class="wp-block-heading">Receiving Request Parts</h3>



<p class="wp-block-paragraph">The <code>HTTPHandler</code> class implements SwiftNIO’s <code>ChannelInboundHandler</code> protocol which expects, among other things, the function <code>channelRead()</code>. This function is called multiple times per request and a value identifying which part of the request is currently being handled is passed via one of three different enums: <code>.head</code>, <code>.body</code> and <code>.end</code>. With these, we can determine our course of action and how to handle the data being sent.</p>



<p class="wp-block-paragraph">One gotcha, however, is that SwiftNIO breaks down large bodies into chunks which means there is the potential for multiple <code>.body</code> parts. I go into more detail about that in the section below where I describe how I implemented the RESTful methods.</p>



<h3 class="wp-block-heading">Bridging to Swift Concurrency via Task</h3>



<p class="wp-block-paragraph">SwiftNIO operates with event loops which, like when working with Node.js, should never be blocked. As such, we need to ensure that route handlers are executed asynchronously. I implemented that by stuffing them inside a Swift <code>Task</code> when SwiftNIO passes <code>.end</code>:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">Task { [head = head] in
    let response = await router.handle(head: head, body: body)
    // ↑ runs on Swift's cooperative thread pool, not the NIO thread
    
    eventLoop.execute {
        self.write(response: response, context: context)
        // ↑ hop back onto the NIO event loop to write the response
    }
}
</code></pre>
</div>



<h3 class="wp-block-heading">Writing The Response</h3>



<p class="wp-block-paragraph">The <code>HTTPHandler</code> class is also responsible for serializing the <code>HTTPResponse</code> object returned by the routes (more on that below) into SwiftNIO’s three-part format which includes:</p>



<ul class="wp-block-list">
<li>HTTPResponseHead (status &amp; headers)</li>



<li>ByteBuffer (body bytes)</li>



<li>End marker (signals that the response is complete)</li>
</ul>



<p class="wp-block-paragraph">This logic takes place in the <code>write()</code> function (<a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L127" target="_blank" rel="noreferrer noopener">see on GitHub</a>):</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">private func write(response: HTTPResponse, context: ChannelHandlerContext) {
    // Allocate a NIO `ByteBuffer` sized to the response body and copy the bytes in.
    var buffer = context.channel.allocator.buffer(capacity: response.bodyData.count)
    buffer.writeBytes(response.bodyData)

    var headers = HTTPHeaders()
    headers.add(name: &quot;Content-Type&quot;, value: response.contentType)
    // Telling the client the exact byte count avoids chunked transfer encoding
    // and lets browsers display progress correctly.
    headers.add(name: &quot;Content-Length&quot;, value: &quot;\(buffer.readableBytes)&quot;)
    headers.add(name: &quot;Connection&quot;, value: &quot;close&quot;)

    let responseHead = HTTPResponseHead(version: .http1_1, status: response.status, headers: headers)

    // Write all three parts. The first two use `write` (buffered); the last uses
    // `writeAndFlush` to push everything to the network in one syscall.
    context.write(wrapOutboundOut(.head(responseHead)), promise: nil)
    context.write(wrapOutboundOut(.body(.byteBuffer(buffer))), promise: nil)

    let channel = context.channel
    context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { _ in
        // Close the TCP connection once all bytes have been sent.
        channel.close(promise: nil)
    }
}
</code></pre>
</div>



<p class="wp-block-paragraph"><code>HTTPResponse</code> is a <code>Sendable</code> struct I created that contains the response HTTP status, the content type and the body data as a Swift <code>Data</code> object. You can view it on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/Router.swift#L16" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Closing The Connection</h3>



<p class="wp-block-paragraph">The <code>write()</code> function above also contains the code that closes the connection after every response. This is the bit of code responsible for doing that:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let channel = context.channel
context.writeAndFlush(wrapOutboundOut(.end(nil))).whenComplete { _ in
    // Close the TCP connection once all bytes have been sent.
    channel.close(promise: nil)
}
</code></pre>
</div>



<h3 class="wp-block-heading">Starting the Server</h3>



<p class="wp-block-paragraph">Now that we have everything in place, all that is left is to instantiate the <code>HTTPServer</code> class and call the <code>start()</code> function at the end of the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/main.swift" target="_blank" rel="noreferrer noopener">main.swift</a> file to start the server:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let server = HTTPServer(host: &quot;127.0.0.1&quot;, port: 8080, router: router)
try await server.start()
</code></pre>
</div>



<p class="wp-block-paragraph">Now that we’ve looked at how the custom server works, let’s see what else the application can do using the server.</p>



<h2 class="wp-block-heading">What This Basic Web App Can Do</h2>



<p class="wp-block-paragraph">I’ve mentioned several times already that the web app I made is basic and simple. It does, however, contain everything necessary for a basic API application or even a web application with light templating needs. Here is an overview:</p>



<ul class="wp-block-list">
<li>RESTful methods</li>



<li>Routing</li>



<li>Basic templating with XSS safety</li>



<li>Static content</li>



<li>HTTP/1.1 support</li>
</ul>



<h3 class="wp-block-heading">RESTful Methods</h3>



<p class="wp-block-paragraph">First off, the application includes support for routes with all RESTful methods (GET, POST, PUT, PATCH, DELETE) including body and header parsing. Initially, I wasn’t going to include support for anything but GET, but SwiftNIO made it surprisingly trivial to include all of them.</p>



<p class="wp-block-paragraph">Supporting body parsing was a bit trickier, but also wasn’t all that difficult once I figured out how SwiftNIO actually handles requests. It doesn’t actually ever give you a complete HTTP request in one go. Instead, it streams it piece by piece. These can be identified via enums like this: <code>.head  →  .body  →  .body  →  ...  →  .end</code>. As you can see though, the <code>.body</code> case can arrive multiple times for large payloads since SwiftNIO breaks it down into chunks. That made it a bit trickier to solve, but also not terrible. I just had to accumulate the chunks and then put them together at the end of the request:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">private var bodyBuffer: ByteBuffer?

// ...

case .body(var buffer):
    // Accumulate body chunks. For small bodies this will be a single call;
    // for larger bodies NIO may deliver multiple chunks.
    if bodyBuffer == nil {
        bodyBuffer = buffer
    } else {
        // Append the new chunk to the existing buffer.
        bodyBuffer!.writeBuffer(&amp;buffer)
    }

case .end:
    if var buf = bodyBuffer, let bytes = buf.readBytes(length: buf.readableBytes) {
        body = Data(bytes)   // ByteBuffer → [UInt8] → Data
    } else {
        body = Data()        // no body (GET, HEAD, etc.)
    }
</code></pre>
</div>



<p class="wp-block-paragraph">The code above is heavily cherry-picked. You can see the whole context on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/HTTPServer.swift#L71" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Routing</h3>



<p class="wp-block-paragraph">Of course, no web application would be worth anything if there wasn’t some form of routing. To accomplish this, I created a <code>Router</code> class that is responsible for registering all routes with their respective methods and handler functions, parsing the incoming URLs and calling said handler functions. The latter always return an <code>HTTPResponse</code> object as described above. I figured the best way to do this would be the regex-route (pun intended) which can be seen in the <code>add()</code> function of the <code>Router</code> class:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">public func add(_ method: HTTPMethod, _ path: String, _ handler: @escaping Handler) {
    var paramNames: [String] = []

    // Replace each `:paramName` segment with a regex capture group `([^/]+)`,
    // and record the parameter name so we can map captures back to names later.
    let regexString = path.replacing(/:([a-zA-Z0-9_]+)/) { match in
        paramNames.append(String(match.output.1))
        return &quot;([^/]+)&quot;
    }

    guard let regex = try? Regex(&quot;^&quot; + regexString + &quot;$&quot;) else { return }
    lock.withLock { routes.append(Route(method: method, regex: regex, paramNames: paramNames, handler: handler)) }
} 
</code></pre>
</div>



<p class="wp-block-paragraph">Then all you have to do is add your routes and handlers in the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/main.swift" target="_blank" rel="noreferrer noopener">main.swift</a> file:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">router.add(.GET, &quot;/&quot;) { head, _, _, _ in
    let pageHtml = layoutView(title: &quot;Homepage&quot;, content: homeView())
    return HTTPResponse(status: .ok, contentType: &quot;text/html&quot;, body: pageHtml.description)
}

router.add(.GET, &quot;/hello/:name&quot;) { head, params, urlComponents, _ in
    let name = params[&quot;name&quot;] ?? &quot;unknown&quot;

    // Extract the optional `?german=true` query parameter.
    let germanParam = urlComponents.queryItems?.first(where: { $0.name == &quot;german&quot; })?.value
    let isGerman = germanParam == &quot;true&quot;

    let title = isGerman ? &quot;Hallo \(name)!&quot; : &quot;Hello \(name)!&quot;
    let pageHtml = layoutView(title: title, content: helloView(name: name, isGerman: isGerman), name: name)

    return HTTPResponse(status: .ok, contentType: &quot;text/html&quot;, body: pageHtml.description)
}
</code></pre>
</div>



<p class="wp-block-paragraph">You might have noticed in the example that parameterization of URLs is also supported. This includes both inline parameters (i.e. <code>/hello/:name</code>) as well as query parameters (i.e. <code>/hello/:name?german=true</code>). Inline parameters are parsed using the regex above and query parameters are extracted from the <code>urlComponents</code> variable injected into the handler function.</p>



<p class="wp-block-paragraph">You can find the entire <code>Router</code> class on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/Router.swift" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Basic Templating with XSS Safety</h3>



<p class="wp-block-paragraph">There were a number of approaches I considered when it came to adding templating to the project. My first instinct was, of course, to reach for one of the off-the-shelf solutions such as <a href="https://docs.vapor.codes/leaf/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Leaf</a>, but I wanted to avoid extra dependencies and keep it as simple as possible. As such, I just decided on simple string interpolation where each template is a standard Swift function that returns a string of HTML. That may not be the best approach for a large web application with a complex frontend, but works perfectly well for this experiment.</p>



<p class="wp-block-paragraph">Fortunately, that also means it’s fairly straightforward. There is a layout view that provides the structure for the site as well as a specific view for each route that should return HTML.</p>



<p class="wp-block-paragraph">The layout view:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">func layoutView(title: String, content: HTML, name: String = &quot;&quot;) -&gt; HTML {
    return &quot;&quot;&quot;
    &lt;!DOCTYPE html&gt;
    &lt;html&gt;
        &lt;head&gt;&lt;title&gt;\(title)&lt;/title&gt;&lt;/head&gt;
        &lt;link rel=&quot;stylesheet&quot; href=&quot;/css/main.css&quot;&gt;
        &lt;script src=&quot;/js/scripts.js&quot;&gt;&lt;/script&gt;
        &lt;body&gt;
            &lt;nav&gt;
                &lt;a href=&quot;/&quot;&gt;Home&lt;/a&gt;
            &lt;/nav&gt;
            &lt;main&gt;\(content)&lt;/main&gt;
            &lt;p&gt;Input your name: &lt;input type=&quot;text&quot; id=&quot;name&quot; value=&quot;\(name)&quot;&gt;&lt;/p&gt;
            &lt;div class=&quot;buttons&quot;&gt;
                &lt;button type=&quot;button&quot; id=&quot;sayHelloButton&quot;&gt;Say Hello!&lt;/button&gt;
                &lt;button type=&quot;button&quot; id=&quot;sayHelloGermanButton&quot;&gt;Say Hello in German!&lt;/button&gt;
            &lt;/div&gt;
        &lt;/body&gt;
    &lt;/html&gt;
    &quot;&quot;&quot;
}
</code></pre>
</div>



<p class="wp-block-paragraph">The <code>/hello/:name</code> route’s view:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">func helloView(name: String, isGerman: Bool = false) -&gt; HTML {
    let greeting = isGerman ? &quot;Hallo \(name)!&quot; : &quot;Hello \(name)!&quot;
    return &quot;&lt;h1&gt;\(greeting)&lt;/h1&gt;&quot;
}
</code></pre>
</div>



<p class="wp-block-paragraph">The layout and route-specific views get put together in each route’s handler. If you look back at the previous examples of adding routes, you’ll see the <code>layoutView()</code> function takes a content parameter. This is where the route-specific view is injected into the layout.</p>



<p class="wp-block-paragraph">You might have also noticed the mysterious <code>HTML</code> type. This doesn’t come from SwiftNIO, but instead is a custom interpolation struct that provides XSS (cross-site scripting) safety. While a little excessive for this particular basic project, I happened to find out while making it that Swift supports such a thing and decided to give it a shot. I won’t go into detail about it here, but you can view the commented struct on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/main/Sources/HTML.swift" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Static Content</h3>



<p class="wp-block-paragraph">If you have templates, you have to have static content as well. What modern website doesn’t have CSS and JavaScript? Even my basic example does, but more as a proof of concept than anything else.</p>



<p class="wp-block-paragraph">Static files are located in the <code>Public</code> folder at the root of the project. The application will automatically figure out the path to it relative to the executable and then return the requested file via a wildcard GET route added to the router after all of the user-defined routes have been added. If no file is found, a 404 “not found” error is returned.</p>



<p class="wp-block-paragraph">This was surprisingly one of the trickier parts of the whole experiment to figure out, but once I had it, it wasn’t terrible to implement. Essentially, I had to read the static file as a Swift <code>Data</code> object and pass it along with the <code>mimeType</code> to the <code>HTTPResponse</code> to be handled in the same way it handles a standard string response:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">guard let data = try? Data(contentsOf: URL(fileURLWithPath: fullPath)) else {
    return HTTPResponse(status: .notFound, body: &quot;Not Found&quot;)
}

let ext = (fullPath as NSString).pathExtension
return HTTPResponse(status: .ok, contentType: mimeType(for: ext), data: data)
</code></pre>
</div>



<p class="wp-block-paragraph">There is a little bit of magic that also happens in there in the form of path sanitization so that a user can’t use standard Unix/Linux paths like &#8220;/..&#8221; to reach other files in the OS:</p>



<div class="wp-block-jetpack-markdown"><pre><code class="language-swift">let sanitized = filePath
    .components(separatedBy: &quot;/&quot;)
    .filter { !$0.isEmpty &amp;&amp; $0 != &quot;..&quot; }
    .joined(separator: &quot;/&quot;)

guard !sanitized.isEmpty else {
    return HTTPResponse(status: .notFound, body: &quot;Not Found&quot;)
}
</code></pre>
</div>



<p class="wp-block-paragraph">You can see the wildcard route and how the files are read on <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless/blob/55c5c2cff25867d5610b187556beb48410a42f06/Sources/Router.swift#L121" target="_blank" rel="noreferrer noopener">GitHub</a>.</p>



<h3 class="wp-block-heading">Only HTTP/1.1 is Supported</h3>



<p class="wp-block-paragraph">One notable limitation of the web application in its current form is that it only supports HTTP/1.1. SwiftNIO also has additional Apple-developed libraries that extend it to support HTTP/2 (<a href="https://github.com/apple/swift-nio-http2" target="_blank" rel="noreferrer noopener">swift-nio-http2</a>) and even an experimental library to allow it to support HTTP/3 (<a href="https://github.com/apple/swift-nio-http3" target="_blank" rel="noreferrer noopener">swift-nio-http3</a>), but I figured sticking with HTTP/1.1 would be good enough for this small experiment.</p>



<h2 class="wp-block-heading">What Else It Could Have Done</h2>



<p class="wp-block-paragraph">I enjoyed creating this project enough that it was hard to keep it simple. I could have just kept tacking on feature after feature until it did everything a standard web application can do, but I was (fairly) disciplined and kept it basic. These are some of the ways I thought it might be nice to expand it and, perhaps, I will fire up a new repo one day to do so.</p>



<p class="wp-block-paragraph">As mentioned above, the server currently only supports HTTP/1.1. It would probably be fairly trivial to add at least support for HTTP/2 with <a href="https://github.com/apple/swift-nio-http2" target="_blank" rel="noreferrer noopener">swift-nio-http2</a>, but I haven’t attempted that yet.</p>



<p class="wp-block-paragraph">Also, a proper templating engine might be useful for a real application. I’ve worked with <a href="https://docs.vapor.codes/leaf/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Leaf</a>, but have also dabbled with <a href="https://github.com/stencilproject/Stencil" target="_blank" rel="noreferrer noopener">Stencil</a> which is another template language for Swift. Either one would be a good choice.</p>



<p class="wp-block-paragraph">Then, of course, rare is the web app that doesn’t have a database behind it. The easiest solution would be to statically link to the operating system’s C SQLite library and call it using raw queries directly in your Swift code. For other databases such as MySQL and PostgreSQL, the makers of Vapor also provide NIO-libraries: <a href="https://github.com/vapor/mysql-nio" target="_blank" rel="noreferrer noopener">MySQLNIO</a> and <a href="https://github.com/vapor/postgres-nio" target="_blank" rel="noreferrer noopener">PostgresNIO</a>. If you would prefer a full-fledged ORM, then <a href="https://docs.vapor.codes/fluent/overview/" target="_blank" rel="noreferrer noopener">Vapor’s Fluent</a> is probably the way to go.</p>



<p class="wp-block-paragraph">However, if you’ve already got Leaf and Fluent, then you might as well save yourself the trouble of everything else and just use <a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a> itself which, as mentioned above, is also based on SwiftNIO.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">So, would I ever use this on an actual production system? Honestly, probably not. For the vast majority of projects, a complete web framework like Vapor makes a lot more sense because it makes development so much faster and easier with much less code to maintain yourself. The performance penalty is so negligible that you probably wouldn’t be able to measure the difference in anything but nanoseconds.</p>



<p class="wp-block-paragraph">However, if you are serving requests at the same scale as Apple, then every nanosecond counts and you might want to consider going with raw SwiftNIO. The margin for error is wide, but the ability to finely tweak performance is vast. Since no project I have ever worked on — professionally or personally — has ever even come close to receiving the amount of requests an Apple service does, I honestly would always go with a framework like Vapor.</p>



<p class="wp-block-paragraph">That said, I did enjoy the experiment and it was interesting to piece together how it works behind the scenes. I could have easily just kept bolting on more features until I had my own mini framework, but I decided to just keep it simple and focus on the core of the application. After all, the point of the experiment was to figure out how it worked and if I could even make it work at all. Fortunately, I was successful in both endeavors.</p>



<p class="wp-block-paragraph">And just as a small aside: The README file in the <a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub repository</a> has a few more technical details than I’ve included here. There are also instructions on how to get it to run and build on Mac (with and without Xcode) as well as Linux. It will only work on Windows with WSL since SwiftNIO relies on POSIX calls for its thread-pooling.</p>



<h2 class="wp-block-heading">Links</h2>



<ul class="wp-block-list">
<li><a href="https://github.com/eiskalteschatten/swift-webapp-frameworkless" target="_blank" rel="noreferrer noopener">GitHub repository with my experiment</a></li>



<li><a href="https://github.com/apple/swift-nio" target="_blank" rel="noreferrer noopener">SwiftNIO</a></li>



<li><a href="https://www.vapor.codes" target="_blank" rel="noreferrer noopener">Vapor</a></li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/29/building-a-web-app-in-swift-using-only-swiftnio/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12418</post-id>	</item>
		<item>
		<title>ScratchBook 2.2 Released</title>
		<link>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/</link>
					<comments>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/#comments</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Mon, 15 Jun 2026 12:09:42 +0000</pubDate>
				<category><![CDATA[Haunted House Software]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[ScratchBook]]></category>
		<category><![CDATA[ScratchPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12400</guid>

					<description><![CDATA[ScratchBook 2.2 has just been released in the Mac App Store with bugfixes and a new menu item to reset text formatting to the default.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">ScratchBook 2.2 has just been released in the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>!</p>



<p class="wp-block-paragraph">Changes include:</p>



<ul class="wp-block-list">
<li>Added a new menu item to reset the selected text&#8217;s or an entire note&#8217;s formatting. You can find it under Format -> Reset Formatting</li>



<li>Fixed a bug where using cmd+option+shift+v didn&#8217;t paste anything</li>
</ul>



<p class="wp-block-paragraph">You can download ScratchBook for free on the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel=" noreferrer noopener"><img data-recalc-dims="1" loading="lazy" decoding="async" width="156" height="40" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=156%2C40&#038;ssl=1" alt="Download on the Mac App Store" class="wp-image-12140" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?w=156&amp;ssl=1 156w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=150%2C38&amp;ssl=1 150w" sizes="auto, (max-width: 156px) 100vw, 156px" /></a></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/15/scratchbook-2-2-released/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12400</post-id>	</item>
		<item>
		<title>The Relaunch of The Old West and Why I Chose Vanilla PHP</title>
		<link>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/</link>
					<comments>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 08:26:38 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[History]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Next.js]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[React]]></category>
		<category><![CDATA[The Old West]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12337</guid>

					<description><![CDATA[I rewrote my project, The Old West, using vanilla PHP despite it not being glamorous. This is why. ]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="654" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11.png?resize=1024%2C654&#038;ssl=1" alt="The Old West in 2026" class="wp-image-12338" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=1024%2C654&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=400%2C256&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=150%2C96&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=768%2C491&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=1536%2C981&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/06/Screenshot-2026-06-02-at-09.38.11-scaled.png?resize=2048%2C1308&amp;ssl=1 2048w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">The Old West in 2026</figcaption></figure>
</div>


<p class="wp-block-paragraph">Two days ago, I relaunched a website that <a href="https://blog.alexseifert.com/2021/04/19/the-old-west/" data-type="link" data-id="https://blog.alexseifert.com/2021/04/19/the-old-west/" target="_blank" rel="noreferrer noopener">I first set up in 2021</a> called <a href="https://www.the-old-west.com" target="_blank" rel="noreferrer noopener">The Old West</a>. It not only features a new design, but also a lot of new content. The purpose of the website is to consolidate Wikipedia articles about the old American west into one spot, making it easy to browse them. I import the articles into my database using Wikipedia&#8217;s API and display them under their&nbsp;<a href="https://en.wikipedia.org/wiki/Wikipedia:Copyrights" target="_blank" rel="noreferrer noopener">Creative Commons Attribution-ShareAlike License</a>. The articles are defined in config files and I wrote a script that imports and updates them. The script is run via a cronjob once a week to keep the articles up to date with the latest changes from Wikipedia.</p>



<p class="wp-block-paragraph">A complete rewrite was long overdue as I could no longer build the old website. At the time, I was heavily into React and <a href="https://nextjs.org" data-type="link" data-id="https://nextjs.org" target="_blank" rel="noreferrer noopener">Next.js</a> which is the technology I chose to power the website. Unfortunately, I also used the <a href="https://mui.com/material-ui/" data-type="link" data-id="https://mui.com/material-ui/" target="_blank" rel="noreferrer noopener">Material UI</a> component library which became the main liability. Material UI released a major new update at some point and I <em>really</em> didn&#8217;t like their new approach to CSS handling. Not only did I dislike it, it would have basically required a major rewrite of the website and all of its CSS. Therefore, I decided not to update. Of course, as time went on, the old version became incompatible with newer versions of React and Next.js and I got stuck with Next.js 12 which is several versions behind.</p>



<p class="wp-block-paragraph">That by itself isn&#8217;t as much of an issue, however, Node itself also continued to move on and eventually, Next.js 12 no longer supported the version of Node I had installed locally. I&#8217;m aware I could have used <a href="https://github.com/nvm-sh/nvm" data-type="link" data-id="https://github.com/nvm-sh/nvm" target="_blank" rel="noreferrer noopener">nvm</a> combined with a specific Node version defined in a <code>.nvmrc</code> file at the root of the project, but that is more than I was willing to do to support the old, increasingly fragile technology powering the project. At that point, I had also become very disillusioned by React, Next.js and the general highly volatile Node ecosystem with its well-known dependency churn and increasing number of supply chain attacks.</p>



<p class="wp-block-paragraph">So, when I decided to finally completely rewrite The Old West, I chose to go in a fully different direction with the technology. I chose vanilla PHP.</p>



<p class="wp-block-paragraph">With so many shiny new programming languages and web frameworks on offer, why did I choose PHP? Well, I didn&#8217;t want dependencies. I wanted to write the code once, deploy it and be able to forget about it for years at a time even if it meant a little bit more work up front. My other requirement was that I still wanted to be able to update it at anytime without having to first spend hours trying to sort out dependency or runtime version incompatibilities just to get it to build again.</p>



<p class="wp-block-paragraph">Vanilla PHP is not only perfect for that, but really the only feasible way to do it. Combined with Apache, Linux and MariaDB (the truly classic LAMP stack), it is practically bulletproof. No supply chain attacks, no version incompatibilities, no dependency update hell &#8212; nothing but stability and peace of mind.</p>



<p class="wp-block-paragraph">PHP is incredibly backwards-compatible which means I can safely update the version on my server using my package manager and don&#8217;t have to worry about too many, if any updates to my code. The only &#8220;dependencies&#8221; I rely on are official PHP extensions such as PDO for database connectivity which I&#8217;m not concerned about because they are maintained and updated with PHP itself. </p>



<p class="wp-block-paragraph">I wrote my own autoloader for namespaces, a tiny, fully custom router module, as well as a very lightweight ORM layer with an inheritable Model class, a QueryBuilder class, and a migration class that processes raw <code>.sql</code> files for migrations. Templating is handled with pure PHP. There is hardly any JavaScript on the website and I use pure CSS without anything like SCSS or Less. I do use the Bootstrap CSS classes for responsiveness, but I copied the CSS files into my project by hand which means they will work forever unless I feel like updating them.</p>



<p class="wp-block-paragraph">That&#8217;s really all there is to it. The website is lightweight, lightning-fast, and entirely customized to my specific needs. It will run stably for as long as the server runs and even if the server craps out at some point, moving the project to another server requires minimal effort.</p>



<p class="wp-block-paragraph">Vanilla PHP may not be glamorous, but it has been the stablest way to create a website for decades and, since it still powers the majority of the web, I think we can safely say it will remain that way for decades to come.</p>



<p class="wp-block-paragraph">If you&#8217;d like to see the new design, you can find The Old West here: <a href="https://www.the-old-west.com" target="_blank" rel="noreferrer noopener">www.the-old-west.com</a>.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/06/02/the-relaunch-of-the-old-west-and-why-i-chose-vanilla-php/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12337</post-id>	</item>
		<item>
		<title>My New 14&#8243; MacBook Pro M5</title>
		<link>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/</link>
					<comments>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Thu, 30 Apr 2026 09:40:24 +0000</pubDate>
				<category><![CDATA[Hardware]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Mac]]></category>
		<category><![CDATA[MacBook]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12271</guid>

					<description><![CDATA[I finally got a new MacBook. It's my first brand new MacBook since 2014.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Well, I finally got a new MacBook Pro. It&#8217;s my first brand new MacBook since I bought a 15&#8243; MacBook Pro in 2014. I&#8217;ve been wanting to buy a new one for several months, but was unsure about whether I should wait for the rumored M6 MacBooks that will supposedly get a new lighter and thinner design later this year or just ignore the rumors and buy a new one now.</p>



<p class="wp-block-paragraph">But once I found out that <a href="https://www.cyberport.de" data-type="link" data-id="https://www.cyberport.de" target="_blank" rel="noreferrer noopener">cyberport.de</a> had MacBooks on sale (I&#8217;m not sponsored), I decided that I couldn&#8217;t wait anymore and bought one. The basic specs are:</p>



<ul class="wp-block-list">
<li>M5 chip</li>



<li>32 GB of RAM</li>



<li>2 TB SSD</li>



<li>14&#8243; display</li>
</ul>



<p class="wp-block-paragraph">I got that for €2500 which is about €500 less than directly from Apple and essentially made the 2 TB SSD upgrade free. I also could have gone with a 16&#8243; instead, but I decided I&#8217;d prefer a larger SSD. Most of the time I use it plugged into an external monitor anyway and when I&#8217;m traveling with it, I like the smaller, lighter form factor.</p>



<p class="wp-block-paragraph">I did play around with the idea of getting a 15&#8243; MacBook Air instead since the specs I care most about are  the same, but in the end I decided the fans might be nice to have even though I&#8217;ll probably only rarely, if ever, need them.</p>



<p class="wp-block-paragraph">Here are a few pictures:</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7151/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro still sealed in the box" class="wp-image-12289" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7151.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro still sealed in the box</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7152/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro still unopened" class="wp-image-12290" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7152.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro still unopened</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7153/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro's first boot screen" class="wp-image-12292" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7153.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro&#8217;s first boot screen</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7154/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro installing macOS 26.4.1" class="wp-image-12291" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7154.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro installing macOS 26.4.1</figcaption></figure>
</div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><a href="https://blog.alexseifert.com/img_7155/"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1024%2C768&#038;ssl=1" alt="My new 14&quot; M5 MacBook Pro transferring files from an external SSD with a Time Machine backup of my old 2019 MacBook Pro" class="wp-image-12293" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/04/img_7155.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">My new 14&#8243; M5 MacBook Pro transferring files from an external SSD with a Time Machine backup of my old 2019 MacBook Pro</figcaption></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/30/my-new-14-macbook-pro-m5/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12271</post-id>	</item>
		<item>
		<title>ScratchBook 2.1 Released</title>
		<link>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/</link>
					<comments>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Tue, 28 Apr 2026 08:13:18 +0000</pubDate>
				<category><![CDATA[Haunted House Software]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[ScratchBook]]></category>
		<category><![CDATA[ScratchPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12257</guid>

					<description><![CDATA[ScratchBook 2.1 has just been released in the Mac App Store and now includes support for Sonoma and Sequoia.]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">ScratchBook 2.1 has just been released in the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>!</p>



<p class="wp-block-paragraph">Changes include:</p>



<ul class="wp-block-list">
<li>Added support for macOS 14 (Sonoma) and macOS 15 (Sequoia)</li>



<li>Fixed an issue with truncated text on the import sheet</li>
</ul>



<p class="wp-block-paragraph">You can download ScratchBook for free on the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel=" noreferrer noopener"><img data-recalc-dims="1" loading="lazy" decoding="async" width="156" height="40" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=156%2C40&#038;ssl=1" alt="Download on the Mac App Store" class="wp-image-12140" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?w=156&amp;ssl=1 156w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=150%2C38&amp;ssl=1 150w" sizes="auto, (max-width: 156px) 100vw, 156px" /></a></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/28/scratchbook-2-1-released/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12257</post-id>	</item>
		<item>
		<title>Examples of Server-Side Swift in the Wild</title>
		<link>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/</link>
					<comments>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Sat, 04 Apr 2026 08:38:41 +0000</pubDate>
				<category><![CDATA[Development]]></category>
		<category><![CDATA[Notes]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[Swift]]></category>
		<category><![CDATA[Vapor]]></category>
		<category><![CDATA[Web Development]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12133</guid>

					<description><![CDATA[Ever since I experimented with the Swift server-side framework Vapor last year, I've been interested in actually seeing it in the wild. Here are a couple of examples.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="640" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1024%2C640&#038;ssl=1" alt="Swift-based architecture for Things Cloud" class="wp-image-12224" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1024%2C640&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=400%2C250&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=150%2C94&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=768%2C480&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?resize=1536%2C960&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/new-backend-overview.png?w=1680&amp;ssl=1 1680w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">Swift-based architecture for Things Cloud</figcaption></figure>
</div>


<p class="wp-block-paragraph">Ever since <a href="https://blog.alexseifert.com/2025/06/29/experimenting-with-the-swift-web-framework-vapor/" data-type="link" data-id="https://blog.alexseifert.com/2025/06/29/experimenting-with-the-swift-web-framework-vapor/" target="_blank" rel="noreferrer noopener">I experimented with the Swift server-side framework Vapor</a> last year, I&#8217;ve been interested in actually seeing it in operation in the wild. I know there have been a number of companies, including Apple itself, that have started using it for their server-side applications, but it&#8217;s nice to see some concrete examples of it.</p>



<p class="wp-block-paragraph">I&#8217;ve been following the <a href="https://www.swift.org/blog/" data-type="link" data-id="https://www.swift.org/blog/" target="_blank" rel="noreferrer noopener">official Swift blog</a> for a while now, but I just recently discovered two posts about companies that are using Swift and Vapor at large scale to run their server-side applications.</p>



<p class="wp-block-paragraph">The first of which is the syncing service for the very popular Mac/iOS productivity application, <a href="https://culturedcode.com/things/" data-type="link" data-id="https://culturedcode.com/things/" target="_blank" rel="noreferrer noopener">Things</a>. I&#8217;ve played around with Things before in the past, but it&#8217;s too expensive for my basic needs, so I don&#8217;t use it. Nonetheless, it is still a very popular application and the fact that Swift and Vapor power its syncing backend is great news for those who are questioning the framework&#8217;s reliability or scalability.</p>



<p class="wp-block-paragraph">You can read more about Things using Swift and Vapor <a href="https://www.swift.org/blog/how-swifts-server-support-powers-things-cloud/" target="_blank" rel="noreferrer noopener">here</a>. The post also includes more information about their infrastructure in general.</p>



<p class="wp-block-paragraph">The second usage I found recently was for the <a href="https://telemetrydeck.com" data-type="link" data-id="https://telemetrydeck.com" target="_blank" rel="noreferrer noopener">TelemetryDeck analytics service</a>. Unlike Things, I had never heard of them, so I can&#8217;t say a whole lot about them, but they apparently have a massive amount of traffic that Swift and Vapor handle flawlessly.</p>



<p class="wp-block-paragraph">You can read about it <a href="https://www.swift.org/blog/building-privacy-first-analytics-with-swift/" target="_blank" rel="noreferrer noopener">here</a>.</p>



<p class="wp-block-paragraph">Of course, something to keep in mind is that both of these posts are on the official Swift blog which is unlikely to report on any negative experiences that companies have had with server-side Swift. However, I still find it great that companies are having success with using Swift and Vapor in production environments with large traffic loads.</p>



<p class="wp-block-paragraph">If you know of any other examples of server-side Swift in the wild, let me know. I&#8217;d love to read about it &#8212; good or bad!</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/04/04/examples-of-server-side-swift-in-the-wild/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12133</post-id>	</item>
		<item>
		<title>Trying to Use My iPad as a Desktop Replacement</title>
		<link>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/</link>
					<comments>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Thu, 26 Mar 2026 15:53:44 +0000</pubDate>
				<category><![CDATA[Featured]]></category>
		<category><![CDATA[Operating Systems]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[Apple]]></category>
		<category><![CDATA[iOS]]></category>
		<category><![CDATA[iPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12163</guid>

					<description><![CDATA[I've already tried using my iPad as a laptop replacement while traveling, but this time, I try to use it as a desktop replacement. This is how it went.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="768" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1024%2C768&#038;ssl=1" alt="My desk setup using my iPad Air M2 in desktop mode" class="wp-image-12184" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1024%2C768&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=400%2C300&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=150%2C113&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=768%2C576&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?resize=1536%2C1152&amp;ssl=1 1536w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/img_7039.jpg?w=2000&amp;ssl=1 2000w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">My desk setup using my iPad Air M2 in desktop mode</figcaption></figure>
</div>


<p class="wp-block-paragraph">A couple of months ago, I took my 11&#8243; iPad Air M2 with me to the city of Regensburg to try <a href="https://blog.alexseifert.com/2026/01/26/trying-to-use-my-ipad-as-a-laptop-replacement/" data-type="link" data-id="https://blog.alexseifert.com/2026/01/26/trying-to-use-my-ipad-as-a-laptop-replacement/" target="_blank" rel="noreferrer noopener">working with it as a laptop replacement</a>. Unfortunately, the tiny 11&#8243; screen and the non-standard, smaller keyboard made it less than a stellar experience. However, I thought I could mitigate those problems while taking advantage of iPadOS 26&#8217;s new windowing feature by using a normal keyboard, mouse and a large external screen &#8212; essentially, turning it into a desktop replacement.</p>



<h2 class="wp-block-heading">Equipment</h2>



<p class="wp-block-paragraph">My equipment list for this experiment isn&#8217;t as exciting as it was for the laptop replacement experiment because I just used my usual desktop accessories:</p>



<ul class="wp-block-list">
<li>Logitech MX Keys keyboard</li>



<li>Apple mouse</li>



<li>LG widescreen monitor that supports USB-C</li>
</ul>



<p class="wp-block-paragraph">Normally, I use a Logitech MX Master 3S mouse instead of the Apple mouse, but I&#8217;ve already used up all three device connections that it supports, so I had dig out my old Apple mouse. Otherwise, the most important part was the monitor with USB-C support. With that, I could just plug my iPad in and it not only activated external display mode, but importantly, it also charged.</p>



<h2 class="wp-block-heading">My Experience</h2>



<p class="wp-block-paragraph">I know that the iPad is limited as to what it is capable of doing primarily due to the limitations of iPadOS. Development is nearly impossible on it which already means it&#8217;s not a viable desktop replacement for me full-time. However, I also write a lot, so I thought the best way to test it was to do some writing.</p>



<p class="wp-block-paragraph">On my Mac, I use Pages anyway and most of it gets saved to iCloud Drive. As such, writing in Pages on the iPad means there isn&#8217;t really much of a change of workflow. I already knew that from when I tried using my iPad as a laptop replacement, but what I didn&#8217;t count on what the severe limitations I ran into when using the mouse and keyboard.</p>



<p class="wp-block-paragraph">It turns out, unsurprisingly, that iPad apps are not optimized for them. That didn&#8217;t surprise me at all, but what did surprise me was just <em>how</em> limited it would be. Even first party-applications like Pages were severely restricted. For example, there is no way to correct a misspelled word with the mouse and keyboard. You have to use the touch screen to tap on the word that is underlined in red and then you get the popup menu with the spelling suggestions. There is absolutely not way to replicate that functionality with the mouse and keyboard which means I had to drag Pages from my big screen down to the iPad&#8217;s touch screen to use it &#8212; not a good experience at all.</p>



<p class="wp-block-paragraph">Also, for some reason, forward delete doesn&#8217;t seem to work at all on the iPad. A full-size Mac keyboard has its own forward delete key and on the smaller Mac keyboards (such as on the MacBooks), you use fn+delete. Neither of those worked on the iPad at all which made trying to edit text much more tedious.</p>



<p class="wp-block-paragraph">Even worse, though, were third-party apps. For example, I tried watching a YouTube video using the YouTube app. I could click on a video, but couldn&#8217;t exit it. There was literally no way to leave the video or access buttons like &#8220;Save&#8221; or &#8220;Like&#8221;. It just didn&#8217;t work at all. Again, I had to drag the app down to my iPad&#8217;s screen and use the touch interface.</p>



<p class="wp-block-paragraph">The worst issue I kept encountering, though, was that the iPad would regularly crash entirely. As in, both screens would go black and there would be a loading spinner on the iPad&#8217;s screen that lasted for several seconds before I could use it again&#8230; except all of the apps I had open were closed. That happened with startling frequency even with only three of my most-used apps open: Mail, Safari and Pages. So much for windowed multitasking.</p>



<p class="wp-block-paragraph">After this happened four times in a half an hour, I gave up on the experiment and went back to my Mac.</p>



<h2 class="wp-block-heading">Conclusion</h2>



<p class="wp-block-paragraph">Needless to say, I won&#8217;t be using my iPad as a desktop or laptop replacement anytime soon. After the experiment with trying to use the iPad as a laptop replacement, I went into this one less than optimistic. Unfortunately, my pessimism was confirmed by the experience.</p>



<p class="wp-block-paragraph">I find it really disappointing that, despite the improvements Apple made to desktop mode in iPadOS 26, it still doesn&#8217;t even come close to being a viable, reliable option. Frankly, my iPad with its M2 processor is several times more powerful than my old Intel MacBook Pro, but the software cripples it to the point of irrelevance.</p>



<p class="wp-block-paragraph">The idea of having a truly all-in-one, portable device is extremely appealing to me. I love using my iPad to read my RSS feeds, eBooks, articles online, magazines, etc which is what I primarily use it for. While I have an Apple Pencil, I rarely use it to take notes or draw anything. Unfortunately, unless Apple makes some major changes to iPadOS, it&#8217;s just going to remain a glorified electronic reader for me.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/03/26/trying-to-use-my-ipad-as-a-desktop-replacement/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12163</post-id>	</item>
		<item>
		<title>Server Stability Issues</title>
		<link>https://blog.alexseifert.com/2026/03/13/server-stability-issues/</link>
					<comments>https://blog.alexseifert.com/2026/03/13/server-stability-issues/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Fri, 13 Mar 2026 09:33:04 +0000</pubDate>
				<category><![CDATA[General]]></category>
		<category><![CDATA[DevOps]]></category>
		<category><![CDATA[Web server]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12152</guid>

					<description><![CDATA[As some of you may have noticed, my websites keep going down and are not reachable for hours at a time. I've been having some stability issues with my server.]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="559" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?resize=1024%2C559&#038;ssl=1" alt="AI-generated image of a server maliciously laughing at a server admin" class="wp-image-12153" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?resize=1024%2C559&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?resize=400%2C218&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?resize=150%2C82&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?resize=768%2C419&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Gemini_Generated_Image_hwm3hihwm3hihwm3.png?w=1408&amp;ssl=1 1408w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">AI-generated image of a server maliciously laughing at a server admin</figcaption></figure>
</div>


<p class="wp-block-paragraph">As some of you may have noticed, my websites keep going down and are not reachable for hours at a time. I&#8217;ve been having some stability issues with my server and, of course, it always happens over night German time which is why it takes me hours to get to it. I only see it when I get up in the morning.</p>



<p class="wp-block-paragraph">Ubuntu has been killing the Apache and MariaDB services due to out-of-memory issues despite my monitoring reporting RAM usage only reaching 20% of the server&#8217;s full capacity. However, I have discovered that there is a massive spike in traffic during that time and I suspect it&#8217;s due to bots. I still need to confirm my theory though.</p>



<p class="wp-block-paragraph">In the meantime, I&#8217;ve reconfigured the Apache and MariaDB services to automatically restart when they die. That should at least solve the immediate problem of my websites being unreachable for hours at a time. I could have sworn that I&#8217;d already set them to automatically restart, but apparently not. Oh well.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/03/13/server-stability-issues/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12152</post-id>	</item>
		<item>
		<title>ScratchBook Now Available on the Mac App Store</title>
		<link>https://blog.alexseifert.com/2026/03/11/scratchbook-now-available-on-the-mac-app-store/</link>
					<comments>https://blog.alexseifert.com/2026/03/11/scratchbook-now-available-on-the-mac-app-store/#respond</comments>
		
		<dc:creator><![CDATA[Alex Seifert]]></dc:creator>
		<pubDate>Wed, 11 Mar 2026 10:19:21 +0000</pubDate>
				<category><![CDATA[Haunted House Software]]></category>
		<category><![CDATA[News]]></category>
		<category><![CDATA[Programs and Tools]]></category>
		<category><![CDATA[Projects]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[ScratchBook]]></category>
		<category><![CDATA[ScratchPad]]></category>
		<guid isPermaLink="false">https://blog.alexseifert.com/?p=12105</guid>

					<description><![CDATA[I'm excited to announce that ScratchBook is now available as a free download on the Mac App Store!]]></description>
										<content:encoded><![CDATA[<div class="wp-block-image">
<figure class="aligncenter size-large"><img data-recalc-dims="1" loading="lazy" decoding="async" width="1024" height="640" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?resize=1024%2C640&#038;ssl=1" alt="ScratchBook screenshot" class="wp-image-12148" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?resize=1024%2C640&amp;ssl=1 1024w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?resize=400%2C250&amp;ssl=1 400w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?resize=150%2C94&amp;ssl=1 150w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?resize=768%2C480&amp;ssl=1 768w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/Full-Screenshot-1.jpg?w=1280&amp;ssl=1 1280w" sizes="auto, (max-width: 1000px) 100vw, 1000px" /><figcaption class="wp-element-caption">ScratchBook screenshot</figcaption></figure>
</div>


<p class="wp-block-paragraph">I&#8217;m excited to announce that ScratchBook is now available as a free download on the Mac App Store!</p>



<p class="wp-block-paragraph">If you are interested in downloading it, you can visit its page on the <a href="https://apps.apple.com/us/app/scratchbook/id6759719038" data-type="link" data-id="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel="noreferrer noopener">Mac App Store</a>.</p>



<p class="wp-block-paragraph">I&#8217;m especially excited about this release because it is the first time I&#8217;ve published anything on any app store and because it&#8217;s a much needed update to the original ScratchPad whose Objective C codebase dates back to Mac OS X Tiger.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://apps.apple.com/us/app/scratchbook/id6759719038" target="_blank" rel=" noreferrer noopener"><img data-recalc-dims="1" loading="lazy" decoding="async" width="156" height="40" src="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=156%2C40&#038;ssl=1" alt="Download on the Mac App Store" class="wp-image-12140" srcset="https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?w=156&amp;ssl=1 156w, https://i0.wp.com/blog.alexseifert.com/wp-content/uploads/2026/03/download-mac-app-store.png?resize=150%2C38&amp;ssl=1 150w" sizes="auto, (max-width: 156px) 100vw, 156px" /></a></figure>
</div>


<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.alexseifert.com/2026/03/11/scratchbook-now-available-on-the-mac-app-store/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">12105</post-id>	</item>
	</channel>
</rss>
