{
    "version": "https://jsonfeed.org/version/1",
    "title": "prass.tech — software development weblog",
    "home_page_url": "https://prass.tech/",
    "feed_url": "https://prass.tech/feed/json",
    "description": "Personal website, portfolio and blog of Christian Praß, software developer based in Germany, writing primarily about technical topics.",
    "author": {
        "name": "Christian Praß",
        "url": "https://prass.tech/"
    },
    "items": [
        {
            "id": "https://prass.tech/blog/rss-full-content-rendering/",
            "content_html": "<p>RSS is pretty cool. We can pick our favorite websites, magazines, blogs, really anything that offers a public feed, add them to our aggregator of choice, and get notified about new content. I use <a href=\"https://netnewswire.com/\" rel=\"noopener\">NetNewsWire</a>, because it has a great UI and lets me sync my reading across Apple devices, but there are many other great options available.</p>\n<p>This blog is built with <a href=\"https://astro.build/\" rel=\"noopener\">Astro</a>. I initially used the recommended <a href=\"https://github.com/withastro/astro/tree/main/packages/astro-rss\" rel=\"noopener\">@astrojs/rss</a> library to generate an RSS feed, but now I want my feed to include the full post content, not just title and description. Astro has a <a href=\"https://docs.astro.build/en/recipes/rss/#including-full-post-content\" rel=\"noopener\">guide</a> for that, but there is one major problem with that approach.</p>\n<blockquote>\n<p>“Note: this will not process components or JSX expressions in MDX files.”</p>\n</blockquote>\n<p>… I’m using MDX. There hasn’t been a clear solution for this problem until recently, when <a href=\"https://astro.build/blog/astro-490/\" rel=\"noopener\">Astro 4.9</a> was released. In this article, I show you exactly how I use the new <a href=\"https://docs.astro.build/en/reference/container-reference/\" rel=\"noopener\">Astro Container API</a> to render the full article content when using <a href=\"https://mdxjs.com/\" rel=\"noopener\">MDX</a>.</p>\n<h2 id=\"the-astro-container-api\">The Astro Container API</h2>\n<p>This new API, at this point still marked as experimental, lets us render a single Astro Component to a string. That’s perfect for generating RSS feeds, because we only need the HTML of the post content, not the whole document structure. Let’s look at the code.</p>\n<pre tabindex=\"0\" data-language=\"ts\"><code>// getPostWithContent.ts\nimport type { APIContext } from 'astro';\nimport { experimental_AstroContainer as AstroContainer } from 'astro/container';\nimport { loadRenderers } from 'astro:container';\nimport { getContainerRenderer as getMDXRenderer } from '@astrojs/mdx';\nimport { render } from 'astro:content';\nimport { rehype } from 'rehype';\n\nexport default async function getPostsWithContent(context: APIContext) {\n  const siteURL = getSiteURL(context);\n\n  const container = await AstroContainer.create({\n    renderers: await loadRenderers([getMDXRenderer()]),\n  });\n\n  const posts = await getSortedBlogPosts();\n\n  return Promise.all(\n    posts.map(async post => {\n      const { Content } = await render(post);\n      const rawContent = await container.renderToString(Content);\n\n      const file = await rehype()\n        .data('settings', {\n          fragment: true,\n        })\n        .use(sanitizeHTML, { siteURL })\n        .process(rawContent);\n      return {\n        post,\n        content: String(file),\n      };\n    })\n  );\n}</code></pre>\n<p>This function is responsible for fetching the post metadata and render the post content to a sanitized string. The <code>siteURL</code> comes from Astro’s <code>APIContext</code> which is available in all API functions. The function will be used inside of a <code>GET</code> request handler, but I’ll get to that later.</p>\n<p>I’m using MDX for my blog posts, so the renderer I need to load for the Astro container is the <a href=\"https://www.npmjs.com/package/@astrojs/mdx\" rel=\"noopener\">MDX renderer</a> (There are more renderers available and you can also write your own). Once the <code>container</code> is created, I load the posts from the content collection and <code>render</code> it to an Astro component.</p>\n<p>At this point I could add the <code>&#x3C;Content /></code> component to a <code>.astro</code> page, but since this is an API function, I pass it to <code>container.renderToString()</code> instead, which renders the Astro component as HTML to a string.</p>\n<p>I could stop here and put this string into the RSS post content, but there are some issues with the HTML output that I have to fix first.</p>\n<h2 id=\"sanitizing-the-output\">Sanitizing the Output</h2>\n<p>Astro, being built for websites, rendered the post for a web page. Links to pages and images of the website use relative paths. Unfortunately that won’t work in RSS readers. To fix this I need to prefix each path with the site URL. To do this properly it requires a three-step process.</p>\n<ol>\n<li>parse the HTML into AST format</li>\n<li>modify some of the nodes</li>\n<li>render the modified AST back into a string</li>\n</ol>\n<p>This sounds like a lot. Parsing and rendering HTML is far from trivial. Lucky for us, there are great tools available. I choose <a href=\"https://unifiedjs.com/\" rel=\"noopener\">unified</a>, or rather <a href=\"https://github.com/rehypejs/rehype\" rel=\"noopener\">rehype</a>, because it’s well-documented and widely used. In fact, it’s used by Astro internally for rendering Markdown.</p>\n<p>I added a single plugin to the processing chain, <code>sanitizeHTML</code>. <code>rehype</code> internally wraps that into a <code>rehypeParse</code>, to turn the HTML string into an abstract syntax tree (AST) and <code>rehypeStringify</code>, which turns the AST into serialized HTML.</p>\n<p>Let me show you the sanitizing plugin.</p>\n<pre tabindex=\"0\" data-language=\"ts\"><code>// sanitizeHTML.ts\nimport type { Element, Root } from 'hast';\nimport type { Plugin } from 'unified';\nimport { visitParents } from 'unist-util-visit-parents';\n\ninterface SanitizeHTMLOptions {\n  siteURL: string;\n}\n\nconst sanitizeHTML: Plugin&#x3C;[SanitizeHTMLOptions], Root> = ({ siteURL }) => {\n  return tree => {\n    visitParents(tree, (node, parents) => {\n      if (node.type !== 'element') {\n        return;\n      }\n\n      // Remove all style tags\n      if (node.tagName === 'style') {\n        return removeElementNode(node, parents);\n      }\n\n      // Remove all script tags\n      if (node.tagName === 'script') {\n        return removeElementNode(node, parents);\n      }\n\n      // Remove all spans inside code tags\n      if (\n        node.tagName === 'span' &#x26;&#x26;\n        parents.some(parent => parent.type === 'element' &#x26;&#x26; parent.tagName === 'code')\n      ) {\n        return removeElementNode(node, parents, true);\n      }\n\n      // Fix relative link URLs\n      if (node.tagName === 'a' &#x26;&#x26; typeof node.properties.href === 'string') {\n        node.properties.href = new URL(node.properties.href, siteURL).href;\n      }\n\n      if (node.tagName === 'a' &#x26;&#x26; 'target' in node.properties) {\n        delete node.properties.target;\n      }\n\n      // Fix relative image URLs\n      if (node.tagName === 'img' &#x26;&#x26; typeof node.properties.src === 'string') {\n        node.properties.src = new URL(node.properties.src, siteURL).href;\n      }\n\n      // Drop all style attributes\n      if ('style' in node.properties) {\n        delete node.properties.style;\n      }\n      // Drop all class attributes\n      if ('className' in node.properties) {\n        delete node.properties.className;\n      }\n\n      // Remove Astros data-astro-cid-... attributes\n      for (const key of Object.keys(node.properties)) {\n        if (key.startsWith('dataAstroCid')) {\n          // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n          delete node.properties[key];\n        }\n      }\n    });\n  };\n};\n\nexport default sanitizeHTML;</code></pre>\n<p>I’m using <a href=\"https://github.com/syntax-tree/unist-util-visit-parents\" rel=\"noopener\">unist-util-visit-parents</a> because it gives me access to the parent nodes of each visited node. <code>removeElementNode</code> is a simple helper function that replaces a node with its children. The plugin does the following sanitization steps.</p>\n<ol>\n<li>Remove all <code>&#x3C;style></code> tags. RSS readers take care of text styling and an RSS feed shouldn’t come with styles attached.</li>\n<li>Remove all <code>&#x3C;script></code> tags, for similar reasons. RSS feeds should provide only text content and semantics and most RSS readers will ignore inline scripts.</li>\n<li>Remove all <code>&#x3C;span></code> tags inside <code>&#x3C;code></code> tags. Astro’s syntax highlighting plugin adds a lot of <code>&#x3C;span></code> tags for styling purposes, but without styles they only add bloat.</li>\n<li>Fix relative link URLs. For example, a link to <a href=\"https://prass.tech/blog/view-transitions\">/blog/view-transitions</a> will be converted to <a href=\"https://prass.tech/blog/view-transitions\">https://prass.tech/blog/view-transitions</a></li>\n<li>Fix relative image URLs for similar reasons.</li>\n<li>Drop inline style attributes.</li>\n<li>Drop all class attributes.</li>\n<li>Remove <code>data-astro-cid-...</code> attributes. Those are used for styling in Astro.</li>\n</ol>\n<p>This will produce minimal clutter-free HTML output.</p>\n<h2 id=\"creating-rss-atom-and-json-feeds\">Creating RSS, ATOM and JSON feeds</h2>\n<p><a href=\"https://github.com/withastro/astro/tree/main/packages/astro-rss\" rel=\"noopener\">@astrojs/rss</a> has no built-in support for Atom feeds. That’s why I decided to use the popular <a href=\"https://www.npmjs.com/package/feed\" rel=\"noopener\">feed</a> library. It can handle RSS 2.0, Atom 1.0 and also JSON Feed 1.0. Perfect!</p>\n<p>In Astro, to create an XML file, we can use a <code>GET</code> handler. I added the following files to the <code>/pages/feed</code> folder: <code>atom.ts</code>, <code>json.ts</code> and <code>rss.ts</code>. The <code>generateFeed</code> function contains the shared logic to create a <code>new Feed()</code>.</p>\n<pre tabindex=\"0\" data-language=\"ts\"><code>import { SITE } from '@config';\nimport getPostsWithContent from '@utils/feed/getPostsWithContent';\nimport type { APIContext } from 'astro';\nimport { Feed, type Author } from 'feed';\nimport getSiteURL from './getSiteURL';\n\nexport async function generateFeed(context: APIContext): Promise&#x3C;Feed> {\n  const siteURL = getSiteURL(context);\n\n  const author: Author = {\n    name: SITE.author,\n    email: SITE.email,\n    link: SITE.website,\n  };\n\n  const feed = new Feed({\n    id: SITE.website,\n    link: siteURL,\n    language: SITE.language,\n    title: SITE.title,\n    description: SITE.desc,\n    favicon: new URL('/favicon.ico', siteURL).toString(),\n    copyright: SITE.license,\n    author,\n    feedLinks: {\n      json: new URL('/feed/json', siteURL).toString(),\n      atom: new URL('/feed/atom', siteURL).toString(),\n      rss: new URL('/feed/rss', siteURL).toString(),\n    },\n  });\n\n  const postsWithContent = await getPostsWithContent(context);\n\n  for (const { post, content } of postsWithContent) {\n    const link = new URL(`/blog/${post.id}/`, siteURL).toString();\n\n    feed.addItem({\n      id: link,\n      link,\n      title: post.data.title,\n      description: post.data.description,\n      published: post.data.pubDate,\n      content,\n      date: post.data.updatedDate || post.data.pubDate,\n      category: post.data.tags.map(tag => ({\n        name: tag,\n        term: tag.toLowerCase(),\n        domain: new URL(`/tags/${tag.toLowerCase()}/`, siteURL).toString(),\n      })),\n    });\n  }\n\n  return feed;\n}</code></pre>\n<p>On each page I added the <code>GET</code> function with the feed output in the <code>Response</code>.</p>\n<pre tabindex=\"0\" data-language=\"ts\"><code>import { generateFeed } from '@utils/feed/generateFeed';\nimport type { APIContext } from 'astro';\n\nexport async function GET(context: APIContext) {\n  const feed = await generateFeed(context);\n\n  return new Response(feed.rss2(), {\n    headers: {\n      'Content-Type': 'application/xml',\n    },\n  });\n}</code></pre>\n<p><em>NOTE: The <code>Content-Type</code> header for <code>feed.json1()</code> is <code>application/json</code> and for <code>feed.atom1()</code> it’s <code>application/atom+xml</code>.</em></p>\n<h2 id=\"discoverability\">Discoverability</h2>\n<p>One more small change is needed to make the feeds discoverable from every page of my website. I’m using a shared layout component, where I added the following lines inside of the <code>&#x3C;head></code> tag.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;link\n  rel=\"alternate\"\n  type=\"application/rss+xml\"\n  title=\"{`${SITE.shortTitle}\"\n  RSS\n  Feed`}\n  href=\"/feed/rss\"\n/>\n&#x3C;link\n  rel=\"alternate\"\n  type=\"application/json\"\n  title=\"{`${SITE.shortTitle}\"\n  JSON\n  Feed`}\n  href=\"/feed/json\"\n/>\n&#x3C;link\n  rel=\"alternate\"\n  type=\"application/atom+xml\"\n  title=\"{`${SITE.shortTitle}\"\n  Atom\n  Feed`}\n  href=\"/feed/atom\"\n/></code></pre>\n<p>And that’s it. Everyone can now read the full article content inside their favorite RSS/ATOM reader.</p>\n<p>Curious to see the live result?</p>\n<ul>\n<li>RSS: <a href=\"https://prass.tech/feed/rss\">https://prass.tech/feed/rss</a></li>\n<li>Atom: <a href=\"https://prass.tech/feed/atom\">https://prass.tech/feed/atom</a></li>\n<li>JSON: <a href=\"https://prass.tech/feed/json\">https://prass.tech/feed/json</a></li>\n</ul>\n<p>I hope you enjoyed this little excursion into the world of RSS and HTML parsing.</p>",
            "url": "https://prass.tech/blog/rss-full-content-rendering/",
            "title": "Astro RSS Feeds with Full MDX Content",
            "summary": "Discover how to use Astro's Container API to include fully rendered MDX content—components and all—in your RSS, Atom, and JSON feeds.",
            "date_modified": "2025-12-07T00:00:00.000Z",
            "date_published": "2025-12-07T00:00:00.000Z",
            "tags": [
                "RSS",
                "Web",
                "Astro"
            ]
        },
        {
            "id": "https://prass.tech/blog/translating-the-ghost-source-theme/",
            "content_html": "<p>The official themes for the Ghost blogging platform usually come with some English texts hard-coded. To fully translate the theme to another language some changes are necessary. Fortunately Ghost comes with a built-in translation system. Let’s take the official default theme “Source” and translate it to German. You can find the open-source theme at <a href=\"https://github.com/TryGhost/Source\" rel=\"noopener\">Github</a>.</p>\n<p>First of all we create the <code>locales</code> folder at the root of the repository. In that folder we create a file named <code>de.json</code> - the name being the ISO 639-1 language code.</p>\n<p>Now we have to find all hard-coded strings and wrap them in the translate helper syntax. Ghost is using Handlebars and the helper function can be used with <code>{{t}}</code>. For example, the file <code>partials/components/navigation.hbs</code> contains the hard-coded string <code>Sign in</code>.</p>\n<pre tabindex=\"0\" data-language=\"diff\"><code>- &#x3C;a href=\"#/portal/signin\" data-portal=\"signin\">Sign in&#x3C;/a>\n+ &#x3C;a href=\"#/portal/signin\" data-portal=\"signin\">{{t \"Sign in\"}}&#x3C;/a></code></pre>\n<p>The parameter of the <code>t</code> function (<code>\"Sign in\"</code>) will be the key of the <code>locales/de.json</code> map. Here are all hard-coded strings that I could find translated into German.</p>\n<pre tabindex=\"0\" data-language=\"json\"><code>{\n  \"Subscribe\": \"Abonnieren\",\n  \"Sign in\": \"Anmelden\",\n  \"Account\": \"Account\",\n  \"Upgrade\": \"Upgrade\",\n  \"Recommendations\": \"Empfehlungen\",\n  \"See all\": \"Alle ansehen\",\n  \"Read more\": \"Ähnliche Artikel\",\n  \"Latest\": \"Neu\",\n  \"By\": \"Von\",\n  \"Close\": \"Schließen\",\n  \"Share\": \"Teilen\",\n  \"Toggle fullscreen\": \"Vollbild\",\n  \"Zoom in/out\": \"Vergrößern/Verkleinern\",\n  \"Previous (arrow left)\": \"Vorheriges (Pfeiltaste links)\",\n  \"Next (arrow right)\": \"Nächstes (Pfeiltaste rechts)\",\n  \"Featured\": \"Ausgewählte Artikel\"\n}</code></pre>\n<p>As you can see, it’s not much. The theme is very minimal. After replacing all the strings in the theme files with the translate helper syntax we can upload the theme.</p>\n<p>This part depends on the kind of Ghost setup we’re using. If it’s a custom installation, we’d usually symlink the theme folder to our Ghost folder. If it’s a managed <a href=\"https://gost.org/\" rel=\"noopener\">https://gost.org</a> installation, we can create a zip file with <code>yarn zip</code> and upload that.</p>\n<p>The last step is to set the <code>Publication Language</code> setting in our Ghost admin dashboard to <code>de</code>. Now we should have a fully translated theme.</p>\n<p><a href=\"https://github.com/cprass/source-german/tree/translate\" rel=\"noopener\">Translated theme code available on Github</a>.</p>",
            "url": "https://prass.tech/blog/translating-the-ghost-source-theme/",
            "title": "Translating the Ghost Source Theme",
            "summary": "The official Ghost CMS Source theme is only available in English, but Ghost has a built-in translation system that makes it easy to translate the theme.",
            "date_modified": "2025-11-17T00:00:00.000Z",
            "date_published": "2025-11-17T00:00:00.000Z",
            "tags": [
                "Ghost"
            ]
        },
        {
            "id": "https://prass.tech/blog/view-transitions/",
            "content_html": "<p>There are two basic types of view transitions. <strong>Single-page transitions</strong> change the structure of a page without changing the location, like opening a dialog box or navigating pages of a typical SPA. <strong>Cross-document transitions</strong> happen when the document is replaced as a whole and a new browser page is loaded.</p>\n<p>Traditionally, single-page transitions were hard to implement and only conveniently usable when utilizing a JS framework that provides a nice and easy high-level API. Cross-document transitions were technically impossible until now, but browsers recently started to implement the <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API\" rel=\"noopener\">View Transition API</a>, providing a new method for both cross-document and single-page (on-page) DOM transitions.</p>\n<p>Cross-document view transitions can be enabled using a single CSS at-rule.</p>\n<pre tabindex=\"0\" data-language=\"scss\"><code>@view-transition {\n  navigation: auto;\n}</code></pre>\n<p>The transition only works if both the current and the next page are on the same origin and both opt-in to the cross-document view transition using the CSS from above. The browser will create a <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition\" rel=\"noopener\"><code>ViewTransition</code></a> instance and a snapshot image of the current page.</p>\n<p>The browser then adds a <a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition\" rel=\"noopener\"><code>::view-transition</code></a> pseudo-element to the top of the root element of the new page. This pseudo-element contains a static snapshot image of the old page and an interactive DOM region for the new page. All view transitions create a simple cross-fade by default. This simplifies the setup a lot. For most cases a cross-fade should be perfectly fine.</p>\n<p>The Chrome devs published some example videos on their <a href=\"https://developer.chrome.com/docs/web-platform/view-transitions/#cross-document_view_transitions\" rel=\"noopener\">view transitions article</a>.</p>\n<h2 id=\"custom-animations\">Custom Animations</h2>\n<p>The animations can be changed using CSS pseudo-selectors and <code>@keyframes</code> sequences.</p>\n<pre tabindex=\"0\" data-language=\"scss\"><code>@keyframes slide-out {\n  from {\n    transform: translateX(0%);\n  }\n\n  to {\n    transform: translateX(-100%);\n  }\n}\n\n@keyframes slide-in {\n  from {\n    transform: translateX(100%);\n  }\n\n  to {\n    transform: translateX(0%);\n  }\n}\n::view-transition-group(root) {\n  animation-duration: 0.3s;\n}\n::view-transition-old(root) {\n  animation-name: slide-out;\n}\n::view-transition-new(root) {\n  animation-name: slide-in;\n}</code></pre>\n<p><code>root</code> is the name of the view-transition added to the root element by default. It’s also possible to target other view-transition elements, change the root transition name or target all transitions using <code>*</code>. The browser always constructs the following tree structure inside the <code>::view-transition</code> root element.</p>\n<pre tabindex=\"0\" data-language=\"plaintext\"><code>::view-transition\n├─ ::view-transition-group(name)\n│  └─ ::view-transition-image-pair(name)\n│     ├─ ::view-transition-old(name)\n│     └─ ::view-transition-new(name)\n└─ …other groups…</code></pre>\n<p>Each view transition creates a group inside the root pseudo-element. Each group has an image pair with the old and new view transition pseudo-elements.</p>\n<h2 id=\"single-element-transitions\">Single Element Transitions</h2>\n<p>Transitioning a single element’s DOM changes is relatively easy. The document updates have to be done inside the <code>document.startViewTransition(...)</code> callback function parameter. <code>startViewTransition</code> returns a <code>ViewTransition</code> object that contains some promises that can be awaited, like <code>viewTransition.finished</code>, to run code after the transition finishes.</p>\n<p>The tricky part is that, if a cross-document transition is enabled, the browser will also do a cross-fade on the DOM regions that are outside the transitioned element. In the following example I had to disable that manually by temporarily disabling the root transition. I also had to use a regular CSS transition for the wrapper height, since the view-transition was only working for the child element.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>document.documentElement.style.viewTransitionName = 'none';\nawait document.startViewTransition(() => {\n  /* ... */\n}).finished;\ndocument.documentElement.style.viewTransitionName = 'root';</code></pre>\n<demo-wrapper> <p><em><strong>Demo:</strong> Clicking on the red div removes it from the parent container and adds a new div to the opposite container.</em></p> <div>  <div id=\"vt-demo-1\"> <div> <div id=\"vt-demo-1-block\">Click me</div> </div> <div></div> </div>  </div> <div> <p> <em>Sorry, your browser doesn't support the <a href=\"https://caniuse.com/view-transitions\" tabindex=\"0\" rel=\"noopener\"> View Transition API </a>.</em> </p> <p><em><a href=\"https://codeberg.org/cprass/gists/src/branch/main/view-transitions-1.astro\" tabindex=\"0\" rel=\"noopener\"> Source Code </a></em></p> </div> </demo-wrapper>    \n<p>The position, color and size transitions are all handled automatically. The browser can find the new element with the same <code>view-transition-name</code> and create a transition matrix to animate between the two positions, even if it’s moved across parent containers.</p>\n<p>The relevant parts for the view-transition are the JS code to handle the DOM update and the CSS to register the <code>view-transition-name</code>.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>document.startViewTransition(() => {\n  // DOM changes\n});</code></pre>\n<pre tabindex=\"0\" data-language=\"scss\"><code>#demo-1-block {\n  view-transition-name: view-transition-demo-1;\n\n  &#x26;.small {\n    width: 100px;\n    height: 100px;\n    background: red;\n  }\n\n  &#x26;.large {\n    width: 200px;\n    height: 200px;\n    background: blue;\n  }\n}</code></pre>\n<h2 id=\"transitioning-multiple-elements-at-once\">Transitioning Multiple Elements at Once</h2>\n<p>Creating a chain of staggered view transitions requires a much more complex setup, both in JS and CSS. Each staggered item needs a unique <code>view-transition-name</code> and the pseudo-element selectors require creating styles for each unique view-transition.</p>\n<demo-wrapper> <p><em><strong>Demo:</strong> Clicking on the red div adds four more elements to the right of it. The elements use a delayed transition animation to create a staggered effect.</em></p> <div>  <div id=\"view-transition-demo-2\"> <div>Click me</div> </div>  </div> <div> <p> <em>Sorry, your browser doesn't support the <a href=\"https://caniuse.com/view-transitions\" tabindex=\"0\" rel=\"noopener\"> View Transition API </a>.</em> </p> <p><em><a href=\"https://codeberg.org/cprass/gists/src/branch/main/view-transitions-2.astro\" tabindex=\"0\" rel=\"noopener\"> Source Code </a></em></p> </div> </demo-wrapper>    \n<p>The <code>view-transition-class</code> CSS property can be used for styling multiple view transitions, but that didn’t quite work for me and I used the following SASS code to generate the CSS for a group of transitions.</p>\n<pre tabindex=\"0\" data-language=\"scss\"><code>@for $i from 0 through 4 {\n  ::view-transition-group(view-transition-demo-1-#{$i + 1}) {\n    animation-duration: 0.2s;\n  }\n  ::view-transition-new(view-transition-demo-1-#{$i + 1}) {\n    animation-name: transition-slide-in;\n    animation-delay: $i * 0.1s;\n  }\n  ::view-transition-old(view-transition-demo-1-#{$i + 1}) {\n    animation-name: transition-slide-out;\n    animation-delay: (4 - $i) * 0.1s;\n  }\n}</code></pre>\n<p>Triggering the view transitions required similar loops in JavaScript.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>document.startViewTransition(() => {\n  for (let i = 1; i &#x3C;= 5; i++) {\n    const div = document.createElement('div');\n    div.classList.add('block');\n    div.style.viewTransitionName = `view-transition-demo-1-${i}`;\n    parent.appendChild(div);\n  }\n});</code></pre>\n<h2 id=\"browser-compatibility\">Browser Compatibility</h2>\n<p>As of right now <a href=\"https://caniuse.com/view-transitions\" rel=\"noopener\">all major browsers except Firefox</a> support the View Transition API. The Firefox devs are <a href=\"https://bugzilla.mozilla.org/show_bug.cgi?id=1823896\" rel=\"noopener\">working on it</a> and the feature will hopefully be released soon.</p>\n<p>Until then, browser compatibility can be checked by wrapping the DOM updates with the view-transition function conditionally.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>if ('startViewTransition' in document) {\n  document.startViewTransition(() => {\n    // ... update DOM with transition\n  });\n} else {\n  // ... update DOM without transition\n}</code></pre>\n<h2 id=\"framework-support\">Framework Support</h2>\n<p>Looking at some of the major frameworks shows that view-transition support seems to be on a good trajectory.</p>\n<ul>\n<li>React has <a href=\"https://react.dev/blog/2025/04/23/react-labs-view-transitions-activity-and-more\" rel=\"noopener\">experimental support</a> with React Router already <a href=\"https://reactrouter.com/how-to/view-transitions\" rel=\"noopener\">supporting view transitions</a></li>\n<li>Next.js provides <a href=\"https://nextjs.org/docs/app/api-reference/config/next-config-js/viewTransition\" rel=\"noopener\">a flag</a> to enable experimental support</li>\n<li>Astro was one of the first major web frameworks to mainstream view transitions and provides <a href=\"https://docs.astro.build/en/guides/view-transitions/\" rel=\"noopener\">many additional features</a> like persisting elements and forwards/backwards navigation animations</li>\n<li>SvelteKit supports view transitions and posted an <a href=\"https://svelte.dev/blog/view-transitions\" rel=\"noopener\">article</a> with examples</li>\n</ul>\n<p>The big picture seems to be that view transitions are being adopted by all major browsers and frameworks. I’m very glad to finally see a standardized way of creating transition animations making its way across browsers and frameworks. I didn’t have to think twice to enable the cross-document default transition on this blog. Now I only have to wait for Firefox support to be able to enjoy it myself.</p>",
            "url": "https://prass.tech/blog/view-transitions/",
            "title": "The View Transition API",
            "summary": "Learn how to create smooth cross-document and single-page transitions using the new View Transition API. This comprehensive guide covers CSS animations, JavaScript implementation, staggered effects, and framework support with practical examples.",
            "date_modified": "2025-07-14T00:00:00.000Z",
            "date_published": "2025-07-14T00:00:00.000Z",
            "tags": [
                "CSS",
                "Web"
            ]
        },
        {
            "id": "https://prass.tech/blog/light-and-dark-theme/",
            "content_html": "<p>Back in the olden days it required a lot of JS to add a dark and light scheme toggle for a website. Now it’s a built-in feature for all modern browsers and operating systems and requires no JavaScript at all, unless a manual toggle button is wanted.</p>\n<h2 id=\"css-only\">CSS Only</h2>\n<p>Most operating systems have internal dark and light schemes and are able to tell the browser and the websites what system scheme is currently active. Browsers also have their own default styles for each scheme and it’s not required to set any colors manually. One line of CSS is all that’s needed.</p>\n<pre tabindex=\"0\" data-language=\"css\"><code>:root {\n  color-scheme: light dark;\n}</code></pre>\n<p>Alternatively, a HTML meta tag can be used.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;meta name=\"color-scheme\" content=\"light dark\" /></code></pre>\n<p>If the user enables dark mode in the OS the website will have a dark scheme. Switching back to light mode in the OS also switches the website to light scheme. When using custom colors a media query can be used to detect the preferred system color scheme.</p>\n<pre tabindex=\"0\" data-language=\"css\"><code>:root {\n  --background: #fefefe;\n  --foreground: #1b1c21;\n  --accent: #12558a;\n}\n\n@media (prefers-color-scheme: dark) {\n  :root {\n    --background: #282a36;\n    --foreground: #f8f8f2;\n    --accent: #8be9fd;\n  }\n}</code></pre>\n<h2 id=\"with-a-theme-toggle-and-classes\">With a Theme Toggle and Classes</h2>\n<p>Sometimes a website uses a light/dark button to allow users to toggle the scheme manually, independent of the OS. This can be achieved with some really simpe JS code.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;button data-theme-toggle title=\"toggle theme\" type=\"button\">\n  &#x3C;span class=\"icon icon-sun\" aria-label=\"toggle dark theme\" />\n  &#x3C;span class=\"icon icon-moon\" aria-label=\"toggle light theme\" />\n&#x3C;/button>\n\n&#x3C;script>\n  // add or remove the class \"dark\" in the root element\n  const cl = document.documentElement.classList;\n  function toggleTheme() {\n    const isDark = cl.contains('dark');\n    cl[isDark ? 'remove' : 'add']('dark');\n  }\n\n  document.querySelector('[data-theme-toggle]')?.addEventListener('click', () => {\n    toggleTheme();\n  });\n&#x3C;/script>\n\n&#x3C;style>\n  :root {\n    --background: #fefefe;\n    /* ... */\n  }\n  :root.dark {\n    --background: #282a36;\n    /* ... */\n  }\n\n  :root.dark .sun {\n    display: none;\n  }\n  :root .moon {\n    display: none;\n  }\n  :root.dark .moon {\n    display: unset;\n  }\n&#x3C;/style></code></pre>\n<p>To make the page set the correct class on the root element on page load, add a script inside of the <code>&#x3C;head></code> element of the page. This is necessary to avoid a light theme flashing on page load.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;head>\n  &#x3C;script>\n    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n      document.documentElement.classList.add('dark');\n    }\n  &#x3C;/script>\n&#x3C;/head></code></pre>\n<h2 id=\"storing-the-selected-theme-in-local-storage\">Storing the Selected Theme in Local-Storage</h2>\n<p>The problem with the manual toggle is that the page will set the default system theme on page load. This is even more problematic on static pages, where every link resets the manually toggled scheme. The solution to this problem is to store the setting in the browsers local storage. If users visit the page after they manually toggled the scheme before, the browser will apply the previous scheme, even if the system has a different default scheme.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>function toggleTheme() {\n  // ...\n  localStorage.setItem('theme', isDark ? 'light' : 'dark');\n}</code></pre>\n<p>This requires small changes to the scheme loader script in the page head.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;head>\n  &#x3C;script>\n    const t = localStorage.getItem('theme') ?? null;\n    if (t === 'dark' || (!t &#x26;&#x26; window.matchMedia('(prefers-color-scheme: dark)').matches)) {\n      document.documentElement.classList.add('dark');\n    }\n  &#x3C;/script>\n&#x3C;/head></code></pre>\n<h2 id=\"issues-in-astro\">Issues in Astro</h2>\n<p>For Astro pages, to avoid scheme flashing on page load it is important to tell Astro not to optimize and bundle the scheme loader script. This can be achieved with the <code>is:inline</code> directive.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;head>\n  &#x3C;script is:inline>\n    const t = localStorage.getItem('theme') ?? null;\n    if (t === 'dark' || (!t &#x26;&#x26; window.matchMedia('(prefers-color-scheme: dark)').matches)) {\n      document.documentElement.classList.add('dark');\n    }\n  &#x3C;/script>\n&#x3C;/head></code></pre>\n<p>I’m currently using the local-storage backed version with manual theme toggle in addition to detecting the system color scheme. I’m going to remove that toggle button and only use the system scheme, getting rid of all additional JavaScript code. The CSS only version completely avoids the scheme flashing issue and makes the website a tiny bit faster and lighter. Dark and light mode toggles are so easy to reach in the OS nowadays. A manual toggle button on the website seems obsolete.</p>",
            "url": "https://prass.tech/blog/light-and-dark-theme/",
            "title": "Light and Dark Scheme for Modern Websites",
            "summary": "Learn how to implement dark and light modes on your website using pure HTML and CSS—no JavaScript required! This guide covers automatic color scheme switching based on the user's system preferences, custom toggles with JavaScript, storing preferences in local storage, and best practices for Astro sites.",
            "date_modified": "2025-06-02T00:00:00.000Z",
            "date_published": "2025-06-02T00:00:00.000Z",
            "tags": [
                "HTML",
                "CSS",
                "Astro"
            ]
        },
        {
            "id": "https://prass.tech/blog/css-containment-context-scrollbar-fix/",
            "content_html": "<p>When creating website layouts it’s sometimes required to escape the parent element’s boundaries horizontally, while keeping vertical flow consistent. One option to achieve this would be to have the horizontal padding and margin only apply to elements containing text, like paragraphs and lists. Defining the padding on a single root element is often easier.</p>\n<p>Sometimes there are multiple layers of padding, margins and borders. Then it can be problematic trying to make a deeply nested element stretch the full width of the viewport. It’d require to know exactly how many pixels the element has to be moved relative to the parent component, to match the document root dimensions.</p>\n<p>On this website I used a width of <code>100vw</code>, combined with negative margin, to make the code blocks stretch the whole window width on smaller screens. Surprisingly, the use of <code>vw</code> lead to an unnecessary horizontal scrollbar appearing.</p>\n<pre tabindex=\"0\" data-language=\"css\"><code>:root {\n  --content-padding: 1rem;\n}\n\n.wrapper {\n  padding: var(--content-padding);\n}\n\npre {\n  margin-left: calc(-1 * var(--content-padding));\n  margin-right: calc(-1 * var(--content-padding));\n  width: 100vw;\n}</code></pre>\n<h2 id=\"container-queries-to-the-rescue\">Container Queries to the Rescue</h2>\n<p>All modern browsers support container queries and the related CSS units like <code>cqw</code> (1% of the width of the closest container). Creating a query container can be done by giving an element the value <code>size</code> or <code>inline-size</code> for the <code>container-type</code> attribute. <code>inline-size</code> creates a query container for the inline axis. Using <code>size</code> creates a query container for both inline and block axis. This means that the container element no longer expands with the content on those axes.</p>\n<p>With a query container the solution is quite simple. Make the <code>body</code> a query container and then refer to it’s width using <code>cqw</code>.</p>\n<pre tabindex=\"0\" data-language=\"css\"><code>body {\n  container-type: inline-size;\n}\n\npre {\n  width: 100cqw;\n}</code></pre>\n<p><strong>Note:</strong> this only works if the <code>body</code> element is the <em>only</em> query container wrapping the <code>pre</code> element. <code>cqw</code> will only use the size related to the <em>closest</em> parent container. There is a proposal for adding a CSS function for each container query unit that allows targeting the query container by name.</p>",
            "url": "https://prass.tech/blog/css-containment-context-scrollbar-fix/",
            "title": "Using CSS Containment Context to Remove a Horizontal Scrollbar",
            "summary": "Creating full-width elements that break out of parent containers can cause horizontal scrollbars. CSS containers provide an easy way to avoid horizontal scrollbars.",
            "date_modified": "2025-05-04T00:00:00.000Z",
            "date_published": "2025-05-04T00:00:00.000Z",
            "tags": [
                "CSS"
            ]
        },
        {
            "id": "https://prass.tech/blog/upgrading-astro-to-5/",
            "content_html": "<p>Upgrading Astro is usually a great experience due to the vast tooling available. There are separate upgrade guides available and there even exists an <a href=\"https://www.npmjs.com/package/@astrojs/upgrade\" rel=\"noopener\">upgrade tool</a> that takes care of the necessary package dependency upgrades. Let’s run that.</p>\n<pre tabindex=\"0\" data-language=\"sh\"><code>npx @astrojs/upgrade\n\n astro   Integration upgrade in progress.\n\n      ◼  @astrojs/check is up to date on v0.9.4\n      ◼  @astrojs/sitemap is up to date on v3.2.1\n      ●  @astrojs/rss will be updated from v4.0.9 to v4.0.11\n      ▲  astro will be updated  from v4.16.16 to v5.2.3\n      ▲  @astrojs/mdx will be updated  from v3.1.9 to v4.0.8\n      ▲  @astrojs/netlify will be updated  from v5.5.4 to v6.1.0\n      ▲  @astrojs/react will be updated  from v3.6.3 to v4.2.0</code></pre>\n<p>I also ran a full dependency upgrade to move to React 19 using <code>npx npm-check-updates -u</code> which worked surprisingly well.</p>\n<p>That was only the first step, though. <a href=\"https://docs.astro.build/en/guides/upgrade-to/v5/\" rel=\"noopener\">https://docs.astro.build/en/guides/upgrade-to/v5/</a> tells us that Astro now uses <code>vite</code> v6.0 and that comes with <a href=\"https://vite.dev/guide/migration.html\" rel=\"noopener\">its own set of breaking changes</a>. I had to get rid of the <a href=\"https://www.npmjs.com/package/@vite-pwa/astro\" rel=\"noopener\"><code>@vite-pwa/astro</code></a>, simply because it doesn’t seem to support Astro v5 yet. I’m not using most of the PWA features and the manifest can be added manually.</p>\n<h3 id=\"no-more-hybrid-mode\">No more <code>hybrid</code> mode</h3>\n<p>We now only have <code>static</code> and <code>server</code> modes and we can opt out from static renderin any time which is a great improvement.</p>\n<h3 id=\"the-content-collections-api-is-now-considered-legacy\">The Content Collections API is now considered “legacy”</h3>\n<p>This is by far the biggest change for me. I knew this was coming and it was surprisingly easy to move to the new Content Layer API, which is supposed to be faster, more versatile and hopefully longer lasting than the old one. I follwed the steps outlined in the guide.</p>\n<ol>\n<li>Moving the <code>src/content/config.ts</code> file to <code>src/content.config.ts</code>.</li>\n<li>Replacing <code>type: 'content'</code> with the loader pattern in <code>content.config.ts</code>.\n<pre tabindex=\"0\" data-language=\"typescript\"><code>loader: glob({ pattern: \"**/*.{md,mdx}\", base: \"./src/content/blog\" }),</code></pre>\n</li>\n<li>Replace the use of <code>post.slug</code> with <code>post.id</code> wherever I load posts.</li>\n<li>Replace <code>post.render()</code> with <code>render(post)</code> as posts are now plain objects without a render method.</li>\n</ol>\n<p>For the sake of future me I did all that, knowing that I didn’t have to because there is a legacy mode available.</p>\n<p>I noticed that my custom slugs still work. I used the <code>slug</code> frontmatter field for that and now Astro automatically picks that as <code>id</code> overwrite. Lucky me! One thing to keep in mind is that the content loader does not guarantee the same order of entries as the legacy loader, so sorting is always a good idea.</p>",
            "url": "https://prass.tech/blog/upgrading-astro-to-5/",
            "title": "Upgrading Astro to Version 5",
            "summary": "A new major version of Astro has landed a while ago and it's time for me to upgrade this website.",
            "date_modified": "2025-02-01T00:00:00.000Z",
            "date_published": "2025-02-01T00:00:00.000Z",
            "tags": [
                "Astro"
            ]
        },
        {
            "id": "https://prass.tech/blog/the-weekly-it-maintenance-day/",
            "content_html": "<p>A couple of months ago, I created a new weekly habit. Ever since, on every Wednesday morning, I go through a list of maintenance tasks to keep my little farm of technological devices content and healthy.</p>\n<p>The idea for that sparked a while earlier. There was a day where I had to wait for updates on mobile apps or operating systems multiple times. This usually doesn’t happen too often, but those updates tend to always show up in the wrong moment and can cause frustration.</p>\n<p>Another even more important reason for doing this is security. I recently started listening to some IT security-related podcasts. That made me more aware of just how often there are crazy vulnerabilities in major software. A good way of reducing the probability of being victim of an exploit, besides being mindful with software usage, is frequent updates.</p>\n<p>Physical maintenance also plays a role, both to keep devices healthy in the long run and to keep me happy, so I don’t have to discover that my laptop battery is dead just at the moment I wanted to use it. That’s why charging devices is part of my weekly routine. Though this only applies to frequently used devices, because for long-term storage, having the battery at full load is a bad idea.</p>\n<p>Without further ado, may I present the current list.</p>\n<ul>\n<li>Restart the router, check for router updates, and have a look across the home network.</li>\n<li>Charge all the Bluetooth devices, like the watch and headphones (might check for updates, but unlikely).</li>\n<li>Charge and restart the phone or tablet, and check for Android or iOS updates.</li>\n<li>On any device with an app store, update apps.</li>\n<li>Charge and restart laptops.</li>\n<li>Check PCs/laptops for OS updates.</li>\n<li>Check all browsers for updates.</li>\n<li>Check virtual OS for updates too, like Linux in WSL.</li>\n</ul>\n<p>This looks like a long list, but it doesn’t actually take too long, and it makes sure everything runs smoothly for the rest of the week. It’s quite common to have a fixed routine for many maintenance tasks at home, like wiping dust, cleaning floors, and cleaning the bathroom, but when it comes to electronic devices, I have the feeling most people don’t like to keep them updated and “clean.”</p>\n<p>There are many more things I can think of integrating into a weekly software maintenance day. Most of us have a digital life that leaves piles of data everywhere. Slowly, digital folders get filled, e-mail inboxes start spanning multiple pages, and the phone is filled with gigabytes of useless, random images. I don’t have a fixed maintenance day for that yet, but maybe some day I will. I believe cleaning up the digital space can be just as refreshing as cleaning up at home.</p>",
            "url": "https://prass.tech/blog/the-weekly-it-maintenance-day/",
            "title": "The Weekly IT Maintenance Day",
            "summary": "I recently started to have a weekly maintenance day where I keep my home devices up to date and in a good working condition.",
            "date_modified": "2024-11-08T00:00:00.000Z",
            "date_published": "2024-11-08T00:00:00.000Z",
            "tags": [
                "IT"
            ]
        },
        {
            "id": "https://prass.tech/blog/my-minimalist-blog-redesign/",
            "content_html": "<p>A few days ago I pushed a commit that reduced the size of this website to about 5 to 10kb in total. Many things have been removed, most files have been refactored. The site now has a full 100 Lighthouse score, is blazingly fast, more mobile friendly and more focused. I removed all elements of the page that haven’t added any meaningful value to the reader, like the dynamic search page, the OG image generation and the paginated archive.</p>\n<p>Before the change, I applied the minimalist philosophy only to the layout and design of the website. Now I went a step further and cleaned up on the code too. The biggest impact for me was the removal of TailwindCSS and React. Both are nice technologies and certainly good for some use-cases, but not so much for a simple blog like this. Tailwind made every piece of HTML look overly complicated and added a ton of CSS that I didn’t need. React wasn’t necessary, because I don’t have much need of client-side JS, now that I removed the search page.</p>\n<p>I didn’t need a CSS reset. Most browsers do a fine job displaying text paragraphs and headings. I now have a single CSS file that’s inlined into the HTML by Astro, because it’s smaller than 4 kb. In fact it’s only about 1kb large. It consists of basic layout and font-size settings, some color rules for the light and dark theme via CSS variables and a few style fixes for blockquotes, code blocks and lists.</p>\n<p>The React components were all migrated to Astro components. The only client-side JS I need is a few lines for storing the theme selection in local storage and dynamically applying the readers preferred color theme. Try setting your OS them to dark mode and the website will trigger the dark theme automatically. Loading React only for that would be overkill. I still want to keep building apps using JS frameworks, but websites are not apps.</p>\n<p>Another thing I removed is the logo. Nobody needs that. The landing page is now simply the full blog archive. No paginated archive anymore, because pagination also isn’t really a necessity.</p>\n<p>I used to have an image generator that created Open Graph images. Not because I really needed it, but because it looked pretty when sharing blog posts at Mastodon. It wasn’t adding anything to the reading experience, so it had to go.</p>\n<p>I’m really happy about the outcome. It lets me focus more on writing and requires less tinkering with the page setup. I think the usability hasn’t suffered a bit. On the contrary, it was improved by better accessibility, a faster loading time and less clutter that distracts from the reading.</p>",
            "url": "https://prass.tech/blog/my-minimalist-blog-redesign/",
            "title": "My Minimalist Blog Redesign",
            "summary": "In the latest update to the code of my blog, I refocused on the essential elements and made the page smaller, lighter and faster, whilst improving the reading experience.",
            "date_modified": "2024-11-03T00:00:00.000Z",
            "date_published": "2024-11-03T00:00:00.000Z",
            "tags": [
                "Meta"
            ]
        },
        {
            "id": "https://prass.tech/blog/100-days-of-swift/",
            "content_html": "<p>This is the story how I got, quite unexpectedly, into to the Swift programming language. It all started on a sunny summer day with a visit to the river.</p>\n<p>I was doing some experiments with long-term exposure with my DSLR camera. If you take a picture of water with a fast shutter speed, you freeze the motion of the water in the image. On the surface of the water you can see every ripple and every reflection. The longer you make the exposure, the more the ripples and water movements blend together until you get a silky smooth and perfectly flat surface, so called motion-blur. If all you do is increasing the exposure, there is one problem. Your picture will be plain white. To solve that, you can use a special kind of filter you put on the lens. It’s called a neutral density filter, or ND-filter for short. It reduces the intensity of the light without changing the colour. Think of sunglasses, but without the tint, just perfectly grey, in most cases.</p>\n<p>ND-filters come in different levels of density. They are denoted using numbers, sometimes with the f-stop number, more on that later, sometimes with the ND-number as a power of two. Mine use the ND-number that denotes the fraction of the light transmitted as a power of two. For example, there is the ND2 filter. ND2 means that it blocks exactly half of the light going through the filter, the same as reducing the size of the lens opening to one half of its original size. This corresponds to the f-stop number 1, we say that the filter reduces the light by one “stop”. If I put that filter on the camera that produced a perfectly exposed image before, and for simplicity I keep the aperture level fixed, I need to double the exposure time to get an image with the same light exposure. This sounds a bit complicated, but it’s actually a very simple mechanism. There is half the amount of light, so we need to double the time to get the same image from or sensor.</p>\n<p>Another example would be the ND1024 filter, which is sometimes called ND1000. It reduces the amount of light to 1/1024, or by 10 f-stops. This means I have to double the exposure time ten times to get a similarly exposed image. Back to the river I was photographing. I set up my aperture and focal length and used the light meter of the camera to set my shutter speed so that I get a perfectly exposed image. Let’s assume it was 1/50 seconds. I put on the ND1000 filter, knowing that it will increase the shutter speed by enough to get as much motion-blur on the water as possible. Taking a picture right now would result in a black image. I have to increase the shutter speed by factor 1024, which is exactly 20.480 seconds. Fortunately my camera has a “bulb” mode where I can set really long exposure times. I took the picture, and it was perfectly exposed with a silky smooth water surface.</p>\n<p>This example may sound like calculating the exposure with ND-filters is an easy thing to do, but it can quickly get really complicated. You can change the aperture too, which affects the exposure just the same as the shutter speed does. You can also combine multiple ND-filters. When standing next to the river ready for shooting a picture, I don’t want to take out a paper, a pen and the calculator to sit down and calculate the exposure time and aperture to use. So I do what every sane person would do - I take out my phone, open the App Store and look for an ND-calculator app.</p>\n<p>Back when I was using Android there was a nice, free app that did everything I wanted, without throwing ads in my face. On the iPhone it’s a different story. I couldn’t find a good app that was not logging all my data, asking for money or was filled with ads. That got me thinking. Since the UI is simple and the app is not more than a special kind of calculator, being a developer by trade, shouldn’t I just build the app myself? Sure, why not?</p>\n<p>The question that followed immediately was the one of progressive web app vs. native app. A PWA would be a good choice. It would be available over the internet, work on desktops, on Android, and as a senior web developer it would be easy to do for me. But there are also downsides to PWAs. They are not as widely supported as they should be and installing them is a process people are rarely used to. Where is the fun in doing something you already know? Native app it is!</p>\n<p>I never cared much for native iOS app development before, so everything about it was new to me. Apparently you use the Swift language to develop Apple apps. Swift is open-source, and the compiler is based on LLVM. Although it is mainly used for apps running on Apple devices, it can target all major platforms, like Windows and Linux. Modern Swift can even run on embedded systems.</p>\n<p>Swift is a surprisingly nice language with many modern features. I think it could be one of the two contenders for a modern, safe C++ alternative, next to Rust. One of the benefits it has over Rust is that it is backed by Apple, in addition to the open-source community. Another one is the major UI framework you get out-of-the-box, SwiftUI. It’s the framework you should probably use to write a iOS app, I suppose.</p>\n<p>SwiftUI uses many of the well-known patterns familiar to anyone working in modern web-development. It comes with a large set of tools to do virtually anything a UI needs. It provides you with overlays, navigations, typography helpers, layout builders, and much more.</p>\n<p>I’m looking forward using all of that soon and a iOS app idea is a good way to improve knowledge with languages and frameworks. But where to start? Diving into app development without any deeper knowledge of a language or the UI toolkit can be daunting. You can lose traction very fast. Browsing the web for resources on learning Swift I came across a website about <strong>100 days of SwiftUI</strong> and I want to use that idea for my own purpose. I might even follow that SwiftUI course, but for now I have other plans. I put together a list of rules to follow in the next couple of months.</p>\n<ol>\n<li>Try to spend exactly 1 hour of programming Swift each day, if possible.</li>\n<li>Use any study source you want, be it coding exercises, reading books, or creating custom apps.</li>\n<li>If you frequently get bored, switch your study source to something more complicated.</li>\n<li>If you frequently get stuck, switch your study source to something simpler.</li>\n<li>Post your progress on Mastodon daily, adding a short reflection on the current day of study.</li>\n</ol>\n<p>I’m currently on day 29 of 100. I am following the Swift track on <a href=\"https://exercism.org/tracks/swift\" rel=\"noopener\">Exercism</a> and I have read parts of the <a href=\"https://docs.swift.org/swift-book/documentation/the-swift-programming-language/\" rel=\"noopener\">official Swift book</a>. Once I get bored with that, I will probably move on to build some CLI ideas on <a href=\"https://projectbook.code.brettchalupa.com/\" rel=\"noopener\">this “projectbook” website</a> I recently discovered.</p>\n<p>Feel free to join me.</p>",
            "url": "https://prass.tech/blog/100-days-of-swift/",
            "title": "100 Days of Swift",
            "summary": "This is a story about how photography got me into learning the Swift programming language. I write about the goals and rules of my 100-Days-Of-Swift project.",
            "date_modified": "2024-07-17T00:00:00.000Z",
            "date_published": "2024-07-17T00:00:00.000Z",
            "tags": [
                "Swift",
                "Photography"
            ]
        },
        {
            "id": "https://prass.tech/blog/reflections-on-becoming-a-self-taught-software-developer/",
            "content_html": "<p>It doesn’t matter what gender you are, what the colour of your skin is or how old you are - there is always an option to become a software developer. Just as there is always a way to become a writer or painter. Software development is similar, because it’s a creative process, and you don’t need to invest anything but time to get started. It’s also very low risk - if you realize it’s not for you, you don’t have to recover from a large pile of debt or material investments.</p>\n<p>In this post I will share with you some reflections on my own self-taught software developer journey.</p>\n<p>The most important part is making the decision that you want to try becoming a software developer. If you read this you might already be at the next step. If not, you might wonder if coding works for you at all. This doesn’t need to be a hard decision, and there is always the option to just try it out and if it doesn’t work out, no harm done. Learning to embrace failure is a core skill of successful developers.</p>\n<h2 id=\"bare-minimum-requirements\">Bare Minimum Requirements</h2>\n<p>Software development doesn’t require much to get started, except a lot of long-term motivation. Additionally, you need a computer and access to the internet. Staying motivated is the hardest part of that journey. It will help a lot if you don’t have a strong aversion towards computers and if you enjoy solving problems, especially the mathematical kind.</p>\n<p>The key to getting started is a smooth transition into coding. You shouldn’t jump into a programming boot-camp, quit your job and try to immediately hit a full-time job at a Fortune-500 company. Don’t enrol in a five-year software development college degree. You learn best by completing small projects, let’s do that instead. Start small and slowly grow taller.</p>\n<h2 id=\"reading\">Reading</h2>\n<p>You will read extremely much. Reading is pretty much all I’m doing the whole work day. You will read code, your own as well as others, you will read the company chat, you will read industry related news and blogs. I highly recommend you get comfortable reading a lot of technical books as well, that will carry you a long way.</p>\n<p>If you don’t like reading at all, you might want to overthink the idea.</p>\n<p>There are some widely recommended books for developers, that are very easy to find by googling a bit. You can also ask a chat AI.</p>\n<h2 id=\"use-the-technology-thats-available\">Use the Technology That’s Available</h2>\n<p>And by technology I mean AI. I can recommend grabbing yourself a Github Copilot subscription, or at least try it out, they offer a free month for testing. GPT, Gemini or whatever AI is popular at the moment, will boost your learning effectiveness a lot.</p>\n<p>Always keep in mind that the answers AIs offer can be plain wrong and the code it generates can be faulty. Even so, it’s very good with explaining code and concepts in programming, giving advice on books to read or podcasts to follow. GPT can be integrated nicely in VSCode nowadays, which will make it very convenient for you to ask code-related questions.</p>\n<h2 id=\"start-projects-you-can-actually-finish\">Start Projects You Can Actually Finish</h2>\n<p>The projects you will learn most from are the ones that you finish and fail. That’s why it’s most important to choose things you can finish. The hype for a prestige project will slowly fade away, and you will get demotivated in the long run. Don’t fall for the stories of people who started that exciting large project idea, finished a successful product and learned to code along the way. Don’t start with a novel, start with many bad and a few good short-stories.</p>\n<p>Talent doesn’t matter. Don’t let anyone tell you that you are not talented enough to become a software developer. It’s all experience and practice. Choose your learning projects depending on your experience. It should be on the difficult side, but not impossible. The harder it is, the better it will feel once you finish it, but the higher the chance you get demotivated and not finish it at all. If you constantly feel bored, don’t bother finishing it. Drop it and pick something more difficult.</p>\n<h2 id=\"compromise-and-trust-your-instincts\">Compromise and Trust Your Instincts</h2>\n<p>At some point you will start your first real programming gig. It could be a small one-time project, an internship, or an actual full-time employment. It will most certainly not work out like you think it would. You will probably be paid much less than you should be paid. And that’s fine.</p>\n<p>If you have little experience it’s okay to get started with projects that don’t pay well, yet you should always strive to avoid being exploited. Once you’re starting to work for real-world projects, you will slowly get an understanding of how much your peers are getting paid.</p>\n<p>You should trust your instincts, show some boldness, but also know that you have to compromise. Sometimes a high salary comes with a price tag. Don’t just follow the money, but choose the job that makes you do what you like to do. Never cut back at your moral principles, only at the salary.</p>\n<h2 id=\"long-term-vs-short-term-gains\">Long-Term vs. Short-Term Gains</h2>\n<p>If you are young and a bit like me in my early developer days, you might fall into the trap of prioritizing your career above everything else. This is to be avoided. It’s easy to fall for the short-term gains, but there are other priorities that are far more important. Your children and family are more important. Your physical and mental health, your financial stability, your pension plan and living situation, they are all more important than climbing the career ladder.</p>\n<p>Working too much is a big problem in our society. Against what most people intuitively think, you actually get more done when working less. I used to work 50 hours a week in my job and used my free-time to learn and study. I did this for more than a year, and although I had a bit more income, I was not in a good mental state. Mistakes happened more often and my social life was in regress. It was only a few years later when I realized that.</p>\n<p>Now I work 32 hours a week and I still use my free-time to study, but not as excessively. I just feel so much better now, live in a happy marriage, and I feel like I get even more quality work done than before. I urge you to try out the 32-hour work week, it’s awesome.</p>\n<h2 id=\"your-company-is-not-your-family\">Your Company is not Your family</h2>\n<p>Companies often want to build a sort of family/clan environment. I can understand where this is coming from, but I don’t think this is a good thing, because it’s a tool that only benefits your management, not you. The company management can use this to increase your feeling of belonging, and with that put pressure on you, without giving your anything in return. It’s also used to create feelings of guilt towards “the family”.</p>\n<p>You should value your company, and especially your colleagues. It’s a good thing to be a motivated employee and to work responsibly. Always try to be aware that it’s a business, not a family. You can be friends with your peers of course, but keep your relations to your managers and bosses on a strict business level.</p>\n<h2 id=\"have-a-pension-plan\">Have a Pension Plan</h2>\n<p>Don’t rely on governments to take care of your pension. Demographics show that the pension system will get much worse in the future, and it’s already in a bad state. Put a small amount of money into an ETF portfolio each month. It should be an index fund, or a mix of them, with a large amount of different shares. A good solution is a so-called “world portfolio” with ETFs like the MSCI World and MSCI Emerging Markets indexes.</p>\n<p>Try to avoid trading single stocks, or speculation in general, as this is the same as gambling away your money. The portfolio should be a very-long-term investment. Only do some rebalancing once in a while and sit out regressions.</p>\n<p>Don’t listen to financial consultants that want to sell you investments. Managed funds don’t beat index funds in the long run. You will have smaller gains and your consultant will even get paid for that.</p>\n<h2 id=\"first-steps\">First Steps</h2>\n<p>If you don’t know how to get started, how about heading over to <a href=\"https://www.freecodecamp.org/\" rel=\"noopener\">freecodecamp.org</a> and learn responsive web design for free?</p>",
            "url": "https://prass.tech/blog/reflections-on-becoming-a-self-taught-software-developer/",
            "title": "Reflections on Becoming a Self-Taught Software Developer",
            "summary": "I share some reflections about the things I learned along my self-taught software developer journey.",
            "date_modified": "2024-03-07T00:00:00.000Z",
            "date_published": "2024-03-07T00:00:00.000Z",
            "tags": [
                "Reflections"
            ]
        },
        {
            "id": "https://prass.tech/blog/datastructures-arrays/",
            "content_html": "<p>Arrays are a basic data structure that can be used in virtually all programming languages. They have the following key features:</p>\n<ul>\n<li>fixed size</li>\n<li>array size and item type must be known upfront</li>\n<li>items are stored in a contiguous memory location</li>\n<li>items must have the same type</li>\n<li>indexed by contiguous positive integers</li>\n</ul>\n<p>I have previously written a blog post about <a href=\"https://prass.tech/blog/arrays-and-vectors-in-c++/\">the basic features of arrays and examples in <code>C++</code></a>, where I describe arrays only in the context of C++. Now I want to take a step back and look at the general data structure of arrays.</p>\n<h2 id=\"creating-arrays\">Creating Arrays</h2>\n<p>When creating an array, think of the memory as blocks, each one has the size of your array type. So if we create an array in C with <code>char* myarray[6];</code>, the program will look at the memory and find the next available free block of 6 bytes, because a <code>char</code> is exactly one byte.</p>\n<p>Let’s assume our memory looks like this, where each <code>[]</code> block contains one byte and <code>[-]</code> means the memory is already taken up by another part of the program.</p>\n<pre tabindex=\"0\" data-language=\"plaintext\"><code> 1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16\n[-] [-] [ ] [ ] [-] [-] [-] [-] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ]</code></pre>\n<p>The second and third block is free, but we need <strong>six</strong> blocks of <strong>contiguous</strong> memory, so that’s not an option. The ninth block is free and the six blocks following it are free too. Sounds like we have found just the right spot. Let’s mark the new reserved memory space for our array with <code>[+]</code>.</p>\n<pre tabindex=\"0\" data-language=\"plaintext\"><code> 1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16\n[-] [-] [ ] [ ] [-] [-] [-] [-] [+] [+] [+] [+] [+] [+] [ ] [ ]</code></pre>\n<h2 id=\"accessing-or-updating-an-array-item-at-an-index\">Accessing or Updating an Array Item at an Index</h2>\n<p>Complexity = O(1)</p>\n<p>The nature of arrays makes it very cheap to access items, because the memory location of each item can be calculated by using the <strong>head</strong> pointer (pointing to the first item), the index and the item type. Let’s assume we have an array defined like this</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>int arr[3] = {1, 2, 3};</code></pre>\n<p>Accessing item <code>2</code> can happen in constant time, because all the machine has to do is to calculate <code>headPtr + typeSize * (index - first-index)</code>. This makes it easy to access even with very large arrays.</p>\n<p>One downside is that we can easily make the mistake of trying to access an index that’s larger than the size of the array. The program will happily abide and give us the random number that happens to live at the given memory location. Although most programming languages have array implementations that protect you from that kind of thin.</p>\n<p>Updating an array item follows the same logic of calculating the address of the item in constant time, regardless of the item position or array size.</p>\n<h2 id=\"insertion-or-deletion-of-array-items-at-an-index\">Insertion or Deletion of Array Items at an Index</h2>\n<p>Complexity = O(n)</p>\n<p>Inserting a new item at an index requires linear time complexity at worst case. To insert a new item at the very first position we have to move every item to the next position, to be able to squeeze the new item in.</p>\n<p>Similarly, to delete an item, we have to move every item after the deleted index one place to the left.</p>\n<p>However, there is a special case of insertion or deletion at the end of the array. This happens in constant time, because there are no items to the right that have to be moved afterwards. (Except for dynamic arrays, where insertion can mean that the whole array has to be re-allocate in a different memory location)</p>\n<h2 id=\"traversing-the-array\">Traversing the Array</h2>\n<p>Complexity = O(n)</p>\n<p>The time complexity of traversing an array is linear, because we can simply move the iterator forward one step at a time until we come to the end of the array.</p>\n<h2 id=\"other-things-to-consider\">Other things to consider</h2>\n<p>There are static and dynamic arrays, with the latter being an abstract data structure. Dynamic arrays can grow and shrink dynamically and re-allocate a larger base-array if the size reaches the capacity limit. Implementations of dynamic arrays are for example <code>vector</code> in C++ and <code>list</code> in Python.</p>\n<p>Static arrays are fixed in size, that means that they can’t grow or shrink. It is necessary to create a new array, copy the items, and free the old array. Dynamic arrays don’t have that limitation, but there it can also be necessary to re-allocate the array.</p>\n<p>Array indexes are often zero-based. Some languages use one-based index and others let you specify the index. Arrays are indexed by contiguous integers.</p>\n<h2 id=\"multi-dimensional-arrays\">Multi-Dimensional Arrays</h2>\n<p>Since arrays are a basic data structure, they can contain other arrays. Technically there is no limit on how deeply arrays are nested.</p>",
            "url": "https://prass.tech/blog/datastructures-arrays/",
            "title": "The Array Data Structure",
            "summary": "Arrays are a common data structure and can be found in some form or another on all programming languages. This post will give you a high level overview of arrays.",
            "date_modified": "2023-11-29T00:00:00.000Z",
            "date_published": "2023-11-29T00:00:00.000Z",
            "tags": [
                "Datastructures"
            ]
        },
        {
            "id": "https://prass.tech/blog/how-to-solve-it-george-polya/",
            "content_html": "<figure><picture><img src=\"https://prass.tech/_astro/how-to-solve-it-george-polya.BOMFoo-E_ZydnjB.webp\"></picture><figcaption>Book cover: \"How to Solve It: A New Aspect of Mathematical Method\" <a href=\"https://press.princeton.edu/books/paperback/9780691164076/how-to-solve-it\" rel=\"noopener\">[source]</a></figcaption></figure><p>“How to Solve It: A New Aspect of Mathematical Method” is a classic work by Hungarian mathematician George Pólya, first published in 1945. The book is a guide to mathematical problem-solving and is written to develop strong problem-solving skills in the reader. It has influenced many areas, including computer science, programming, and artificial intelligence.</p>\n<p>Even today Pólya’s book appears on many reading lists about software, maths and algorithms, although the writing style feels antiquated. “How to Solve It” is, for the most part, a dialogue between teacher and student. Pólya doesn’t just try to teach the reader how to solve problems, but also how to become a better teacher. He shows how to interact with students and what questions to ask, to make them solve problems and learn to solve them on their own.</p>\n<p>The book revolves around the four steps of the problem-solving method.</p>\n<h2 id=\"the-pólya-method\">The Pólya Method</h2>\n<h3 id=\"1-understanding-the-problem\">1. Understanding the Problem</h3>\n<p>Understanding a problem is not as easy as it seems at first. Pólya’s approach is to ask the right questions. The problem needs to be divided into its parts — condition, data, and the unknown. Those can be divided further by asking if the condition is sufficient to determine the unknown or even contradictory.</p>\n<p>At this stage, it often helps to draw a figure, write down the data, or restate the problem with other words. This leads to building a good understanding of the problem. The problem statement can often be abstract and it’s important to separate the meaningful and unnecessary parts of the data.</p>\n<h3 id=\"2-coming-up-with-a-plan\">2. Coming Up With a Plan</h3>\n<p>After getting familiar with a problem, Pólya advises the reader to think deeply about it. He tries to think of anything that sparks ideas on how to solve it. Sometimes that involves a related problem or a way of working backwards from the solution.</p>\n<p>At this point, the author recommends the use of heuristics. Those are different practical approaches to problem-solving that aren’t ideal or optimal, but effective. There are many heuristics described in the book. Some are specific to a certain kind of problem, others can be generally applied to almost any kind. The approach could involve solving auxiliary problems, using logic, trying to find a pattern or plain and simple guessing.</p>\n<h3 id=\"3-executing-the-plan\">3. Executing the Plan</h3>\n<p>This stage of the four-step process is all about strictly following the plan developed in the previous step. Each step must be clearly understood and executed. This includes formally proving that the solution is correct.</p>\n<h3 id=\"4-looking-back\">4. Looking Back</h3>\n<p>Revising the work is a crucial and often overlooked step of the problem-solving process. Not only is it important to check if the solution is correct, but also to verify if the problem was solved completely and no part was missed. But Pólya urges the reader to do more than that. The solution should be reconsidered to see what could be done differently. Finally, this presents an opportunity to think about other problems that this solution could be used for.</p>\n<h2 id=\"the-classroom-and-the-method\">The Classroom and the Method</h2>\n<p>Early in the book George Pólya explains his philosophy of the classroom, how to interact with the students and how to talk and motivate them. He gives many mathematical examples of how to guide the student through the whole process. This always starts with finding the unknown and concludes with looking back at the solution and trying to find a better or more interesting way of getting there.</p>\n<p>The author cycles through different steps of the method, each time exploring different heuristics and examples. This is done by asking a lot of questions from the perspective of the teacher and then trying to act upon those questions from the perspective of the student.</p>\n<h2 id=\"a-short-dictionary-of-heuristic\">A Short Dictionary of Heuristic</h2>\n<p>For the largest part of the book, Pólya writes about key points of heuristics. It’s written in the form of a dictionary. There is a lot of repetition and asking the same question already presented in the earlier parts.</p>\n<p>Following the dictionary of heuristics is a set of problems, hints and solutions for the reader to exercise the four-step problem-solving process.</p>\n<h2 id=\"my-personal-thoughts\">My Personal Thoughts</h2>\n<p>The book shows that Pólya was a man of both wisdom and intellect. His language is clear and concise. The wording of the book is dated, but easy enough to understand. The writing style takes a while to get comfortable with. It is mostly an imaginary discussion between the teacher and the student.</p>\n<p>The simple dialogue style makes abstract processes easy to understand, and Pólya delivers a lot of knowledge while making sure it’s repeated often enough for everything to sink in. The problem-solving process described is not a mind-blowing revelation, though. Most of the approaches are well-known to anyone working in a technical job involving maths, but it can be beneficial to read about them from a different point of view.</p>\n<p>Problem-solving is a vital skill for any software developer and it comes naturally with experience. “How to Solve It” is an interesting perspective, but not an essential read. It’s a good book to pick up for those interested in general mathematical problem-solving or the philosophy of teaching it.</p>\n<h2 id=\"more-information\">More Information</h2>\n<ul>\n<li><a href=\"https://en.wikipedia.org/wiki/How_to_Solve_It\" rel=\"noopener\">Wikipedia</a></li>\n<li><a href=\"https://www.goodreads.com/book/show/192221.How_to_Solve_It\" rel=\"noopener\">Goodreads</a></li>\n</ul>",
            "url": "https://prass.tech/blog/how-to-solve-it-george-polya/",
            "title": "Book Review: \"How to Solve It\" by G. Pólya",
            "summary": "A book review of the classical work by Hungerian mathematician George Pólya.",
            "date_modified": "2023-10-29T00:00:00.000Z",
            "date_published": "2023-10-29T00:00:00.000Z",
            "tags": [
                "Books"
            ]
        },
        {
            "id": "https://prass.tech/blog/astro-syntax-highlighting/",
            "content_html": "<p><a href=\"https://astro.build/\" rel=\"noopener\">Astro</a> comes with <a href=\"https://docs.astro.build/en/reference/configuration-reference/#markdownsyntaxhighlight\" rel=\"noopener\">Shiki syntax highlighting out of the box</a>, which is great. But it’s missing a core feature you get in most IDEs by default: line numbers.</p>\n<p>Not only do they provide you a sense of position in the document, but they also help when lines get wrapped over. Then you’ll see a gap in the number sequence and intuitively know that this line belongs to the one above.</p>\n<h2 id=\"its-css-only\">It’s CSS Only</h2>\n<p>The awesome part is that this can be achieved with CSS only.</p>\n<p><em>NOTE: I use the Tailwind CSS integration for Astro. If you don’t use Tailwind, you can just translate those classes to CSS directly.</em></p>\n<p>My solution is based on <a href=\"https://github.com/shikijs/shiki/issues/3#issuecomment-830564854\" rel=\"noopener\">this Github comment</a>, kudos to the author.</p>\n<p>Add the following lines to your global CSS rules.</p>\n<pre tabindex=\"0\" data-language=\"css\"><code>pre {\n  @apply rounded-md pl-8;\n}\n\npre code {\n  @apply block leading-tight p-2 pl-1;\n  font-size: 17px;\n  border-left: 1px solid rgba(115, 138, 148, 0.4);\n  counter-reset: step;\n  counter-increment: step 0;\n}\n\npre code .line {\n  @apply relative;\n}\n\npre code .line::before {\n  @apply absolute overflow-hidden w-7 h-4 -left-9 top-0 text-right;\n  content: counter(step);\n  counter-increment: step;\n  color: rgba(115, 138, 148, 0.4);\n}</code></pre>\n<p>That’s it!</p>\n<h2 id=\"bonus-round---sass-like-rule-nesting\">Bonus Round - SASS-like Rule Nesting</h2>\n<p>I find the previous approach a little verbose. It would be much nicer to just nest the CSS rules into each other. Astro ships with <a href=\"https://docs.astro.build/en/guides/styling/#postcss\" rel=\"noopener\">PostCSS by default</a>, so there is no reason why we shouldn’t be able to do it. However, the issue with using it together with TailwindCSS is, that it can’t handle the tailwinds special syntax out of the box. Luckily Tailwind provides us with a <a href=\"https://docs.astro.build/en/guides/styling/#postcss\" rel=\"noopener\">compatibility layer</a> for PostCSS nesting.</p>\n<p>For this to work we need to take control over the PostCSS config. We can do this by specifying a <code>postcss.config.cjs</code> file in the project root with the following contents.</p>\n<pre tabindex=\"0\" data-language=\"js\"><code>module.exports = {\n  plugins: {\n    'postcss-import': {},\n    'tailwindcss/nesting': {},\n    tailwindcss: {},\n    autoprefixer: {},\n    cssnano: {},\n  },\n};</code></pre>\n<p>I kept the other things, like <code>autoprefixer</code>, <code>postcss-import</code> and <code>cssnano</code> because they were provided in the examples and I think they are also used by Astro’s default config, but I’m not sure about that. Anyhow, they sound like a good idea to have. For this we need to install those modules, because if we use it, it should be declared as a dependency.</p>\n<pre tabindex=\"0\" data-language=\"bash\"><code>npm install -D postcss-import autoprefixer cssnano</code></pre>\n<p>Now we can enable nesting together with our TailwindCSS <code>@apply</code> syntax. It doesn’t make it shorter, but it helps a lot with readability and structuring the code.</p>\n<pre tabindex=\"0\" data-language=\"sass\"><code>pre {\n  @apply rounded-md pl-8;\n\n  code {\n    @apply block leading-tight p-2 pl-1;\n    font-size: 17px;\n    border-left: 1px solid rgba(115, 138, 148, 0.4);\n    counter-reset: step;\n    counter-increment: step 0;\n\n    .line {\n      @apply relative;\n\n      &#x26;::before {\n        @apply absolute overflow-hidden w-7 h-4 -left-9 top-0 text-right;\n        content: counter(step);\n        counter-increment: step;\n        color: rgba(115, 138, 148, 0.4);\n      }\n    }\n  }\n}</code></pre>",
            "url": "https://prass.tech/blog/astro-syntax-highlighting/",
            "title": "Adding Line Numbers to Astro's Syntax Highlighting Using CSS",
            "summary": "Astro uses Shiki syntax highlighting by default. It works great but it's missing one core feature, line numbers. In this article I explain how to add line numbers using CSS only and making it work with TailwindCSS.",
            "date_modified": "2023-10-04T00:00:00.000Z",
            "date_published": "2023-10-04T00:00:00.000Z",
            "tags": [
                "Astro",
                "CSS"
            ]
        },
        {
            "id": "https://prass.tech/blog/arrays-and-vectors-in-c++/",
            "content_html": "<figure><picture><img src=\"https://prass.tech/_astro/2023-09-bars-and-arrows.CVOUC7kD_Z2dAK6p.webp\" alt=\"An array of bars for securing bicycles.\"></picture></figure><p>If you have been programming before, you have most likely used arrays. They can be found in most of the modern programming languages. This post is going to give you an overview of arrays and then dive into the C++ <code>vector</code> class.</p>\n<h2 id=\"the-array-data-structure\">The Array Data Structure</h2>\n<blockquote>\n<p>In computer science, an array is a data structure consisting of a collection of elements (values or variables), of same memory size, each identified by at least one array index or key. An array is stored such that the position of each element can be computed from its index tuple by a mathematical formula. The simplest type of data structure is a linear array, also called one-dimensional array. <a href=\"https://en.wikipedia.org/wiki/Array_(data_structure)\" rel=\"noopener\">Wikipedia</a></p>\n</blockquote>\n<p>Thanks, Wikipedia! One key point is that an array is stored in a continuous space in memory. That, combined with the fact that the types of data stored in the values must be of the same data type, means that it becomes very easy to access array indices or iterate over them. Arrays store the <em>base address</em> and together with the fixed size of values, so the memory address of a value at index <code>i</code> can be easily computed with <code>base-addr + i * size</code>.</p>\n<p>That means arrays have constant lookup time, no overhead per array item, almost no overhead for the array itself and make it very fast to access, iterate over or copy parts of the array. For more complex tasks like inserting items, sorting and searching, there are of course more efficient data structures, most of which are based on arrays.</p>\n<p>Arrays are sometimes referred to as:</p>\n<ul>\n<li>tuple</li>\n<li>vector</li>\n<li>matrix</li>\n<li>tensor</li>\n<li>table</li>\n</ul>\n<h2 id=\"arrays-in-cc\">Arrays in C/C++</h2>\n<p>Arrays in C use a zero-based index. They are a core data structure with a fixed size and values stored in a continuous memory location. C++ can use C-based arrays, but it also has an own <a href=\"https://en.cppreference.com/w/cpp/container/array\" rel=\"noopener\"><code>array</code></a> class with more functionality similar to vectors.</p>\n<h3 id=\"declaration-initialization-and-assignment-of-basic-arrays\">Declaration, Initialization and Assignment of Basic Arrays</h3>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>// simple declaration\nint a[10];\n// initialization\nint b[] = { 1, 2, 3, 4, 5 };\n// initializing all elements to zero\nint c[10] = {};\n// assigning values to elements\na[0] = 1;\na[1] = 2;</code></pre>\n<p>Declaration can be done by specifying the type of the array values, followed by the variable name, followed by square brackets containing the array size. The size can be omitted when initializing the array with values.</p>\n<p>When declaring an array without initializing the values, then the values will not be zero. They will have the value that has been on that memory address before. The array variable is a pointer to the address of the first element, so let’s have a look.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>int a[1];\n\n// This prints the address of the pointer to the first array element.\nstd::cout &#x3C;&#x3C; a &#x3C;&#x3C; endl; // 0x7ffcc66069c0\n\n// This prints the value of the first element. It can be any integer value.\n// The value will be whatever value had been on that memory address range before.\nstd::cout &#x3C;&#x3C; a[0] &#x3C;&#x3C; endl; // 72704\n// We could also dereference the pointer to get the value of the first element.\nsrd::cout &#x3C;&#x3C; *a &#x3C;&#x3C; endl; // 72704</code></pre>\n<h3 id=\"array-bounds\">Array Bounds</h3>\n<p>C/C++ does not check array bounds when accessing values, it simply does not care.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>int a[] = { 5, 6 };\n\ncout &#x3C;&#x3C; a[0] &#x3C;&#x3C; \", \" &#x3C;&#x3C; a[1] &#x3C;&#x3C; \", \" &#x3C;&#x3C; a[2] &#x3C;&#x3C; endl;</code></pre>\n<p>This will print <code>5, 6, -81237</code> with the last value being any random integer that happens to be at the memory location right after the last array element.</p>\n<h2 id=\"vectors-in-c\">Vectors in C++</h2>\n<p><a href=\"https://en.cppreference.com/w/cpp/container/vector\" rel=\"noopener\">Vectors</a> are very similar to arrays in C++, with the key difference that they can grow and shrink dynamically. They are based on arrays, which means they are also stored in a continuous memory location and can be addressed by calculating the pointer based on the type and index.</p>\n<p>One might think that adding items to vectors would mean the whole array has to be moved to a different, larger memory location, but that’s actually not always the case. Vectors usually allocate a bit more space than they need, so they can grow without moving the contents around.</p>\n<p>The name “vector” is a bit misleading. Mathematically, a vector is like a matrix with only a single row and a fixed size. Vectors are sized dynamically and also heap allocated. Apparently the goal with the naming was to strongly indicate the difference between regular arrays and vectors.</p>\n<h3 id=\"performance-considerations\">Performance Considerations</h3>\n<p>Vectors are relatively efficient when adding or removing elements at their end or accessing elements. They are not very efficient when adding or removing elements that are not at the end or the vector. For efficient iteration, insertion or deletion of any middle element there are other types that are better suited.</p>\n<h3 id=\"using-vectors\">Using Vectors</h3>\n<p>Vectors are part of the <a href=\"https://en.cppreference.com/w/cpp/standard_library\" rel=\"noopener\">C++ Standard Library</a>, more specifically of the <a href=\"https://en.cppreference.com/w/cpp/container\" rel=\"noopener\">Containers Library</a>.</p>\n<p>There are many ways of creating vectors.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>#include &#x3C;vector>\n\nint main() {\n    vector&#x3C;int> v1; // creates an empty vector\n    vector&#x3C;int> v2(3); // creates a vector with size 3\n    vector&#x3C;int> v3{1, 2, 3}; // creates a vector with an initializer list\n    vector&#x3C;int> v4(v3); // creates a vector, copying the elements from v3\n    vector&#x3C;int> v5(v4.begin(), v4.end()); // creates a vector, copying the elements from v4\n    vector&#x3C;int> v6(3, 4); // creats a vector with size 3 and fills it with copies of the value 4\n}</code></pre>\n<p>Vectors can be iterated over just like arrays.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>#include &#x3C;vector>\n\nint main() {\n    vector&#x3C;int> v{1,2,3};\n\n    for (auto i : v) {\n        std::cout &#x3C;&#x3C; i;\n    }\n\n    for (int i = 0; i &#x3C; v.size(); i++) {\n        std::cout &#x3C;&#x3C; v[i];\n    }\n}</code></pre>\n<h3 id=\"useful-vector-methods\">Useful Vector Methods</h3>\n<p>The vector class provides a lot of methods to interact with an instance. I will go over a few of the methods as provided on the <a href=\"https://cplusplus.com/reference/vector/vector/#functions\" rel=\"noopener\">C++ reference</a> page.</p>\n<h4 id=\"capacity\">Capacity</h4>\n<p>Vectors have both a capacity and a size. The system will reserve a certain amount of memory that can be larger than the actual size, so the vector doesn’t have to be reallocated constantly when adding new elements.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>size_t size() const noexcept;\nsize_t capacity() const noexcept;</code></pre>\n<p><code>size()</code> returns the number of items in the vector while <code>capacity()</code> returns the number of items that can fit in the allocated space. Capacity is always equal or greater than size.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>void resize(size_t n);\nvoid resize(size_t n, const value_type&#x26; val);</code></pre>\n<p><code>resize()</code> changes the size of the vector to <code>n</code>, either dropping elements at the end or filling the missing space with copies of <code>val</code> or the default constructor, if <code>val</code> is not provided.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>bool empty() const noexcept;</code></pre>\n<p><code>empty()</code> returns <code>true</code> if the vector is empty and <code>false</code> if it’s not. This does not modify the vector, for that you can use <code>clear()</code>;</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>void reserve(size_t n);</code></pre>\n<p><code>reserve()</code> allocates space for <code>n</code> vector items. The resulting vector capacity can be equal to or greater than <code>n</code>.</p>\n<h4 id=\"element-access\">Element Access</h4>\n<p>Vectors provide a few additional methods to access elements besides the commonly used <code>operator[]</code>.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>reference at(size_t n);\nconst_reference at(size_t n) const;</code></pre>\n<p><code>at()</code> is similar to <code>operator[]</code> operator, it returns the element at the given index. The key difference is that <code>at()</code> performs a boundary check and will raise an exception if the index is out of bounds.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>reference front();\nconst_reference front() const;\nreference back();\nconst_reference back() const;</code></pre>\n<p>Quite simple, <code>front()</code> returns the first element in the vector and <code>back()</code> returns the last element. The docs state that using this on an empty vector causes <em>undefined behavior</em>, which sounds a tiny bit scary.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>value_type* data() noexcept;\nconst value_type* data() const noexcept;</code></pre>\n<p>Earlier I stated that vectors are based on arrays and <code>data()</code> gives you a pointer to the first value of the underlying array. It can be used just like any other array.</p>\n<h4 id=\"modifiers\">Modifiers</h4>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>void assign(iterator first, iterator last); // range\nvoid assign(size_t n, const value_type&#x26; val); // fill\nvoid assign(initializer_list&#x3C;value_type> il); // initializer-list</code></pre>\n<p><code>assign()</code> destroys the current content of the vector and replaces it with new content. The three versions work similar to the constructor versions used in the <a href=\"https://prass.tech/#using-vectors\">example above</a>.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>void push_back(const value_type&#x26; val);\nvoid push_back(value_type&#x26;&#x26; val);</code></pre>\n<p><code>push_back()</code> lets you add an element to the end of the vector at <em>amortized</em> constant complexity. <em>Amortized</em> because reallocation may happen, and it happens with complexity linear to the size of the vector.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>void pop_back();</code></pre>\n<p><code>pop_back()</code> simply removes the last element of the vector and destroys it, reducing the vector size by one. This happens with constant complexity. Using this on an empty vector is another case of <em>undefined behavior</em>.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>iterator insert(iterator position, const value_type &#x26;val); // copy\niterator insert(iterator position, value_type &#x26;&#x26;val); // move\niterator insert(iterator position, iterator first, iterator last); // range\niterator insert(iterator position, size_t n, const value_type&#x26; val); // fill\niterator insert(iterator position, initializer_list&#x3C;value_type> il); // initializer-list</code></pre>\n<p><code>insert()</code> inserts one or multiple elements into the vector <em>before</em> the element at <code>position</code> and returns an iterator pointing to the first of the inserted elements. Inserting at any place other than the end of the vector is deemed inefficient compared to other types of storage. It’s quite similar to the constructor call signatures, or <code>assign()</code> (minus the <code>position</code> parameter). You can pass a value as copy or move, two iterators, an initializer list or an amount and fill value.</p>\n<pre tabindex=\"0\" data-language=\"cpp\"><code>iterator erase(iterator position);\niterator erase(iterator start, iterator end);</code></pre>\n<p><code>erase()</code> removes either a single element at <code>position</code> or a range of elements from <code>first</code> to <code>last</code>. It returns an iterator pointing to the element after the removed element(s), which can be the <code>end()</code> of the vector.</p>\n<h2 id=\"further-reading\">Further Reading</h2>\n<ul>\n<li><a href=\"https://en.wikipedia.org/wiki/Array_(data_structure)\" rel=\"noopener\">https://en.wikipedia.org/wiki/Array_(data_structure)</a></li>\n<li><a href=\"https://en.wikipedia.org/wiki/Array_(data_type)\" rel=\"noopener\">https://en.wikipedia.org/wiki/Array_(data_type)</a></li>\n<li><a href=\"https://cplusplus.com/doc/tutorial/arrays/\" rel=\"noopener\">https://cplusplus.com/doc/tutorial/arrays/</a></li>\n<li><a href=\"https://www.geeksforgeeks.org/cpp-arrays/\" rel=\"noopener\">https://www.geeksforgeeks.org/cpp-arrays/</a></li>\n<li><a href=\"https://cplusplus.com/reference/vector/vector/\" rel=\"noopener\">https://cplusplus.com/reference/vector/vector/</a></li>\n</ul>",
            "url": "https://prass.tech/blog/arrays-and-vectors-in-c++/",
            "title": "Arrays and Vectors in C++",
            "summary": "My notes and examples on arrays and vectors and how to use them in C++.",
            "date_modified": "2023-09-26T00:00:00.000Z",
            "date_published": "2023-09-26T00:00:00.000Z",
            "tags": [
                "C++",
                "Datastructures"
            ]
        },
        {
            "id": "https://prass.tech/blog/installing-applications-in-ubuntu/",
            "content_html": "<p>I tried to install the Firefox Developer Edition on my Ubuntu laptop, because … well, I’m a developer, and it integrates some neat features. I also feel like the standard Firefox is sometimes very slow when loading development builds of websites and opening the debugger.</p>\n<p>If you go to the <a href=\"https://www.mozilla.org/de/firefox/developer/\" rel=\"noopener\">Firefox Developer Edition website</a> you get a huge download button which downloads a <code>.tar.bz2</code> archive when clicked. I couldn’t find the app on the <code>apt</code> or <code>snap</code> repos, so that’s what I did.</p>\n<pre tabindex=\"0\" data-language=\"sh\"><code>tar -xvf firefox-118.0b8.tar.bz2</code></pre>\n<p>Extracting this gave me a folder including the <code>firefox-bin</code> executable file and a bunch of other stuff. Running that file with a shell opens the browser, sweet! But … wait a minute! Do I have to open a terminal and search that one file every time I want to start a browser window? And what about the dock? Right-clicking the app icon doesn’t show the “add to favorites” option, what’s going on?</p>\n<h2 id=\"adding-a-desktop-entry\">Adding a Desktop Entry</h2>\n<p>When manually installing apps like this it seems to be necessary to add what’s called a “desktop entry”. If you want to go all nerd on this one, I recommend you read the official <a href=\"https://specifications.freedesktop.org/desktop-entry-spec/latest/\" rel=\"noopener\">Desktop Entry specification</a>. There are two places where those entries usually live at. The first is for the system-wide entries at <code>/usr/local/share/applications</code>, but in my case I wanted the local user configuration at <code>$HOME/.local/share/applications</code> since I installed the app in my home directory. I added a <code>firefox-dev.desktop</code> file in there with the following content:</p>\n<pre tabindex=\"0\" data-language=\"toml\"><code>[Desktop Entry]\nType=Application\nName=Firefox Developer\nGenericName=Firefox Developer Edition\nExec=/home/me/Applications/firefox/firefox-bin %u\nIcon=/home/me/Applications/firefox/browser/chrome/icons/default/default128.png\nTerminal=false\nCategories=Application;Network;X-Developer;\nComment=Firefox Developer Edition Web Browser.\nStartupWMClass=firefox-aurora</code></pre>\n<p>It seems like you can’t use <code>$HOME</code> or <code>~</code> in the path. For me, it only worked after I’ve written out the full path explicitly.</p>\n<p><code>/home/me/Applications/firefox</code> is where I extracted the archive to. If the folder is at a different path, the entries for <code>Exec</code> and <code>Icon</code> have to be changed.</p>\n<p>And that was finally all that’s necessary to set it up. Now, when using the quick-search in Ubuntu, Firefox Developer Edition shows up correctly. I can also finally add it to my apps-dock. Take that, Chrome!</p>\n<h2 id=\"final-thoughts\">Final thoughts</h2>\n<p>While it is quite easy to do when you’re an “advanced” Linux user (whatever that means), this stuff is incredibly hard to understand for beginners. I feel like in 2023 it should be much easier to install an application in Linux. I’m on Ubuntu 22.04, which is the latest official LTS version at the time of writing and I expected that adding a <code>.desktop</code> file to some <code>~/.local/share</code> folder would not be necessary. I also expected to be able to use some easy to understand GUI tool for adding applications, especially when it includes executable and icon paths and odd things like a <code>StartupWMClass</code>. “Odd” because it would mess everything up if it’s not called exactly <code>firefox-aurora</code> for reasons unknown to me.</p>\n<p>If there is an easier way, let me know. Maybe I just missed it somehow.</p>",
            "url": "https://prass.tech/blog/installing-applications-in-ubuntu/",
            "title": "Installing Applications in Ubuntu the Hard Way",
            "summary": "I'm going through the steps to install Firefox Developer Edition in Ubuntu which includes adding a .desktop file, which is a bit more than I expected to be required.",
            "date_modified": "2023-09-13T00:00:00.000Z",
            "date_published": "2023-09-13T00:00:00.000Z",
            "tags": [
                "Linux"
            ]
        },
        {
            "id": "https://prass.tech/blog/road-to-responsive-images/",
            "content_html": "<p>When I wrote my first blog article for this website, I needed an image to spice things up a bit. So I did what any naive frontend developer would do, I added a <code>&#x3C;img></code> tag, put my image in the <a href=\"https://astro.build/\" rel=\"noopener\">Astro</a> public folder and published the changes. I had tested the website in local development, and after it was deployed it looked pretty good on my desktop PC using a 250 MBit/s internet connection.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;img src=\"/images/img.jpg\" alt=\"naive approach\" /></code></pre>\n<h2 id=\"optimizing-the-base-image\">Optimizing the Base Image</h2>\n<p>Not too much time later I realized that I had uploaded an image larger than 1mb and about 2000px in width. My site had a layout that fixed the content to 640px at that time. The easy way out of that hole was to start properly optimizing images before the upload. Using Imagemagick’s <a href=\"https://imagemagick.org/script/convert.php\" rel=\"noopener\">Convert</a> tool makes that pretty easy. I figured a 90% quality setting should be fine. Luckily my image (downloaded from my Flickr gallery) was already at 90%, so I just needed to change the size.</p>\n<pre tabindex=\"0\" data-language=\"bash\"><code>convert myimage.jpg -resize 800x myimage2.jpg</code></pre>\n<p>Down to 120kb, so around 10 times smaller than before. But what about the file format? JPEG is quite old and there are quite a few modern formats that are designed to replace it. WebP for example.<a href=\"https://developers.google.com/speed/webp?hl=en\" rel=\"noopener\">According to Google</a> it’s a good choice and on average about 25-34% smaller than JPEG - at least for lossy images. It’s widely supported, so why not use that instead?</p>\n<pre tabindex=\"0\" data-language=\"bash\"><code>convert myimage2.jpg myimage.webp</code></pre>\n<p>Down to 90kb, nice! Now I just had to make sure I can really use that instead of JPEG. <a href=\"https://caniuse.com/webp\" rel=\"noopener\">Browser support</a> looks promising since I don’t really care about IE support. Now there was yet another problem. If I wanted to make use of OpenGraph or Twitter extensions, I had to use JPEG, PNG or GIF. So what now?</p>\n<figure> <picture> <source srcset=\"/_astro/2023-09-caniuse-webp.zzOrvXb0_1G5QvK.jpg 1200w\" type=\"image/jpeg\" sizes=\"(min-width: 760px) 760px, 100vw\"><source srcset=\"/_astro/2023-09-caniuse-webp.zzOrvXb0_1NACHm.webp 760w,/_astro/2023-09-caniuse-webp.zzOrvXb0_Z5uyAw.webp 540w\" type=\"image/webp\" sizes=\"(min-width: 760px) 760px, 100vw\"> <img loading=\"lazy\" decoding=\"async\" alt=\"\" src=\"https://prass.tech/_astro/2023-09-caniuse-webp.zzOrvXb0_1G5QvK.jpg\" height=\"531\" width=\"1200\"> </picture> <figcaption> Browser support table for WebP images, 09/23   </figcaption> </figure> \n<h2 id=\"picture-to-the-rescue\"><code>&#x3C;picture></code> to the Rescue!</h2>\n<blockquote>\n<p>“The &#x3C;picture> HTML element contains zero or more &#x3C;source> elements and one &#x3C;img> element to offer alternative versions of an image for different display/device scenarios.” <a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture\" rel=\"noopener\">MDN</a></p>\n</blockquote>\n<p>That sounds incredibly useful, and it’s supported in all major browsers, what a nice surprise!</p>\n<figure> <picture> <source srcset=\"/_astro/2023-09-caniuse-picture.HsrNaN9q_Z1ykhJv.jpg 1200w\" type=\"image/jpeg\" sizes=\"(min-width: 760px) 760px, 100vw\"><source srcset=\"/_astro/2023-09-caniuse-picture.HsrNaN9q_ZNu4BO.webp 760w,/_astro/2023-09-caniuse-picture.HsrNaN9q_1n9uuH.webp 540w\" type=\"image/webp\" sizes=\"(min-width: 760px) 760px, 100vw\"> <img loading=\"lazy\" decoding=\"async\" alt=\"\" src=\"https://prass.tech/_astro/2023-09-caniuse-picture.HsrNaN9q_Z1ykhJv.jpg\" height=\"531\" width=\"1200\"> </picture> <figcaption> Browser support table for the &#x3C;picture> tag, 09/23   </figcaption> </figure> \n<p>Using it seems pretty straightforward. I can specify multiple <code>&#x3C;source></code> elements, each one containing a <code>type</code> attribute declaring the <a href=\"https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types\" rel=\"noopener\">MIME-type</a>. When used inside a <code>&#x3C;picture></code>, you have to use <code>srcset</code> and you can alternatively use a <code>media</code> attribute, but more on that later.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;picture>\n  &#x3C;source srcset=\"img.webp\" type=\"image/webp\" />\n  &#x3C;img src=\"img.jpg\" alt=\"we're getting there\" />\n&#x3C;/picture></code></pre>\n<p>Cool! So all of this sounded quite complicated, but now I have a 90kb image that will be used by all modern browsers and a fallback to JPEG if it’s needed. The funny thing is that this is not even close to the ideal solution not the most complicated part, but I promise we’ll get there in a bit.</p>\n<p>Today screens have evolved a lot and are no longer the typical LCD screens with 100ish dpi that you still see a lot in laptops or even desktop computers. Most phones and tablets and more and more desktop screens have a higher resolution and a higher pixel density. To make up for that, browsers zoom the pages to reduce multiple hardware pixels into a single CSS-pixel. That’s why you see the following part a lot in mobile-optimized pages.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" /></code></pre>\n<p>This works well for text and layout in general, but not so much for bitmap images. Depending on the device pixel ratio, some devices can display images up to twice as large in the same space.</p>\n<h2 id=\"providing-multiple-sources-using-srcset-and-sizes\">Providing Multiple Sources Using <code>srcset</code> and <code>sizes</code></h2>\n<p>The <code>srcset</code> attribute lets you specify a list of image sources and one additional piece of information that is either the pixel density that the file should be used for or the actual width of the image in pixels. For very basic use-cases the pixel density is ok, but for responsive layouts that’s not going to work very well. Luckily it can be used together with the <code>sizes</code> property to give the browser all it needs to determine what source to use.</p>\n<p>The <code>sizes</code> attribute tells the browser in what size the image is displayed on the page, using media conditions. The browser can then use that information together with the <code>srcset</code> list to fill that gap with the best matching image from the sources. Now you don’t even have to take care of the pixel density anymore, because the media queries already take that into account (…at least that’s how I assume it works).</p>\n<p>Here is an example using both attributes that would already work pretty good for full-screen images.</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;picture>\n  &#x3C;source\n    srcset=\"myimg-640.webp 640w, myimg-1024.webp 1024w, myimg-1200.webp 1200w\"\n    sizes=\"100vw\"\n    type=\"image/webp\"\n  />\n  &#x3C;img src=\"myimg.jpg\" alt=\"looking good so far\" />\n&#x3C;/picture></code></pre>\n<p>But of course it can’t be that easy for my use-case. My blog has no full-screen image, but it uses like 95% of the width up to a specific width where it stops growing and stays fixed.</p>\n<p>I even added some additional breakpoints for images just to make it harder. Well, actually I wanted to replicate how Medium can display images that grow larger than the text column. I like that look. It improves images on larger screens, because now you see more of the content instead of having a lot of whitespace left and right.</p>\n<h2 id=\"choosing-sizes\">Choosing <code>sizes</code></h2>\n<p>My current solution for sizes looks something like this:</p>\n<pre tabindex=\"0\" data-language=\"html\"><code>&#x3C;picture>\n  &#x3C;source\n    srcset=\"\n      .../myimg.webp?width=2400&#x26;f=webp 2400w,\n      .../myimg.webp?width=1600&#x26;f=webp 1600w,\n      .../myimg.webp?width=1200&#x26;f=webp 1200w,\n      .../myimg.webp?width=960&#x26;f=webp   960w,\n      .../myimg.webp?width=640&#x26;f=webp   640w\n    \"\n    sizes=\"\n    (min-width: 1300rem) 1200px,\n    (min-width: 860px) 800px,\n    (min-width: 768px) 640px,\n    (min-width: 648px) 576px,\n    calc(100vw - 72px)\"\n    type=\"image/webp\"\n  />\n  &#x3C;img src=\"fallback.jpg\" loading=\"lazy\" alt=\"its over 9000\" />\n&#x3C;/picture></code></pre>\n<p>In the first moment this looks quite complicated, but I promise you it will all make sense soon. Remember when I told you about the fixed max-width of my page content before? I said it was <code>640px</code> and that number sits right there in the <code>sizes</code> list. The media condition is specific to my layout. Between the left and right viewport borders there is some padding, so what I’m telling the browser here is that between <code>648px</code> and <code>768px</code> viewport size it should use the next best image that fits into <code>640px</code> from the <code>srcset</code>. This would be the image with <code>960px</code> width, or it could even be the one with <code>1600px</code> width, depending on the device pixel ratio. I hope now you can understand how <code>sizes</code> is used and how it closes the gap between image sizes and page layout.</p>\n<h2 id=\"am-i-overdoing-it\">Am I Overdoing It?</h2>\n<p>Having a fully responsible image setup is cool, but it also comes with a downside. For every image I want to use in an article I now have about ten images that need to be created and uploaded. Luckily Astro provides a great feature for <a href=\"https://docs.astro.build/en/guides/images/#generating-images-with-getimage\" rel=\"noopener\">automating image conversion and formatting</a>. After building an own component for that I am able to import a single base image in my blog post and Astro converts that into the full list of sources with <code>srcset</code> and <code>sizes</code> props. I will soon post an article about how to do that, so stay tuned!</p>\n<p>Converting those images still takes time and CPU resources and my blog is currently not very large. I’m sure the build time will get notably longer once I have more than a hand-full of posts with images. But if that ever becomes a problem I might just reduce the number of images created.</p>\n<h2 id=\"the-media-attribute\">The Media attribute</h2>\n<p>Sometimes it’s necessary to use a different image for certain breakpoints. This would be a problem, because the <code>srcset</code> is just a suggestion for the browser, not a strict directive. Luckily there is the <code>media</code> attribute for <code>&#x3C;source></code> elements. It lets us add a media condition that is required for this source to be picked by the browser. This makes it the ideal option for design directives, like using a cropped image for mobile devices and the full-sized version for larger screens.</p>",
            "url": "https://prass.tech/blog/road-to-responsive-images/",
            "title": "The Taxing Road to Responsive Images",
            "summary": "Todays browsers support responsive images out of the box and for the best user experience it is necessary to serve images responsively. It's not trivial to implement a more advanced solution. This is the story about why and how I serve a multitude of formats and sizes for images on this blog.",
            "date_modified": "2023-09-03T00:00:00.000Z",
            "date_published": "2023-09-03T00:00:00.000Z",
            "tags": [
                "HTML",
                "CSS",
                "Astro",
                "Meta"
            ]
        },
        {
            "id": "https://prass.tech/blog/adventure/",
            "content_html": "<figure><picture><img src=\"https://prass.tech/_astro/2023-08-road-by-the-river-in-summer.CUuXERpn_1uH9nn.webp\" alt=\"A photo showing a stone path overgrown with grass and trees.\"></picture></figure><p>The time has finally come. All the months thinking about it, polishing my website design, changing unnecessary things and embarking on side projects for procrastination are now over. This will be the first entry transforming this website officially into a blog. Now it may be the first and also the last entry, who knows. So many projects have been started with high hopes and abandoned soon after. Let me just write a few words about what you can expect, what my reasons behind writing are, and maybe I’ll add some ramblings about blogging in general.</p>\n<h2 id=\"setting-expectations\">Setting expectations</h2>\n<p>My interests will probably be what’s defining the shape of this blog. I’m a web developer by trade and I do a lot of development and learning on the side, so you can expect a lot of those topics. I’m also digging into photography, pentesting, general IT security stuff and reading a lot of sci-fi and fantasy books. Hell, maybe I’ll even share some notes about city-trips or hikes, who knows?</p>\n<p>When it comes to development I like to build small learning projects in the form of games or tools to get into new languages or frameworks. This page is a good example, because it’s the project I started to get into <a href=\"https://astro.build/\" rel=\"noopener\">the Astro framework</a>. I also recently built a Minesweeper clone using <a href=\"https://fresh.deno.dev/\" rel=\"noopener\">Deno’s Fresh framework</a> and exploring <a href=\"https://preactjs.com/guide/v10/signals\" rel=\"noopener\">Preact signals</a> as a pretty cool alternative to React’s state management.</p>\n<h2 id=\"why-does-the-world-need-another-blog\">Why does the world need another blog?</h2>\n<p>Journaling made me a better person and probably improved my life by an order of magnitude. I’ve done daily journaling for a long period in my life and it was very good for improving mood, sorting thoughts, recollection and personal introspection. In the last years the writing has ebbed down a bit, so this blog, even though it’s not a personal journal, will make me get some writing done. Furthermore, it will help with refining my English, because this is not my primary language.</p>\n<p>Now those are some very personal reasons for writing, so why does “the world” need another blog exactly? I think self-hosted blogs are a good example of a decentralized and free internet. It also serves as an example of how easy it can be to host a blog nowadays. Astro + Github pages and it’s done. I don’t need a Wordpress server if I can just write my blog entries in a Markdown file.</p>\n<p>You can even host that completely for free on Github pages, as long as you make the repository public. That means you can have a complete website with blog on your custom domain for the cost of a cup of coffee per year, which sounds pretty amazing.</p>\n<h2 id=\"tldr\">TL;DR</h2>\n<p>Writing is fun. And nobody can stop me from doing it, obviously.</p>\n<h2 id=\"ps-book-recommendation\">PS: Book Recommendation</h2>\n<p>The title is a quote from the book I’m currently reading: <a href=\"https://www.goodreads.com/book/show/40376072-children-of-ruin\" rel=\"noopener\">Adrian Tchaikovsky’s “Children of Ruin”</a>. Grab a copy, it’s fun, I promise! It’s the sequel of <a href=\"https://www.goodreads.com/book/show/25499718-children-of-time\" rel=\"noopener\">“Children of Time”</a>, which I recommend reading first.</p>",
            "url": "https://prass.tech/blog/adventure/",
            "title": "We Are Going on an Adventure",
            "summary": "This is the first post of my new blog. I write about my reasons and motivation for blogging, what to expect from this blog in the future and some offtopic things.",
            "date_modified": "2023-08-16T00:00:00.000Z",
            "date_published": "2023-08-16T00:00:00.000Z",
            "tags": [
                "Meta"
            ]
        }
    ]
}