<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ethan Hawksley&apos;s Blog</title><description>Technical blog of Ethan Hawksley, a UK-based CS student. Articles on systems programming, low-level computing, cybersecurity, and computer science.</description><link>https://hawksley.dev/</link><language>en-GB</language><copyright>Content licensed under CC BY 4.0</copyright><atom:link href="https://hawksley.dev/rss.xml" rel="self" type="application/rss+xml"/><item><title>JSON-LD Explained for Personal Websites</title><link>https://hawksley.dev/blog/json-ld-explained-for-personal-websites/</link><guid isPermaLink="true">https://hawksley.dev/blog/json-ld-explained-for-personal-websites/</guid><description>Everything you need to know about JSON-LD and how to implement it.</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;JSON-LD, also known as JSON Linked Data, is a format for adding structured data to webpages. It can aid web crawlers in understanding the semantic structure of your site, qualifying you for richer link previews, and even potentially improving your search ranking.&lt;/p&gt;
&lt;p&gt;It&amp;#39;s been 4 months since my first post where I described building this site, and Wakatime estimates I&amp;#39;ve spent ~100 hours coding now, not including time spent researching and testing. Since then, this site has been receiving plenty of polish, including the addition of JSON-LD on each page.&lt;/p&gt;
&lt;h2&gt;JSON-LD Fundamentals&lt;/h2&gt;
&lt;p&gt;To add JSON-LD to a page, add the following somewhere in your &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;
  {
    &amp;quot;@context&amp;quot;: &amp;quot;https://schema.org&amp;quot;,
    &amp;quot;@graph&amp;quot;: [
      {
        &amp;quot;@type&amp;quot;: &amp;quot;WebSite&amp;quot;,
        &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;,
        &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
        &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;
      },
      // Insert more nodes here.
    ]
  }
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s break down what each part does.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&amp;lt;script type=&amp;quot;application/ld+json&amp;quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This declares a new script with MIME type &lt;code&gt;application/ld+json&lt;/code&gt;. Since it has this type specified, the browser&amp;#39;s JS engine won&amp;#39;t run it. Specialised crawlers like Googlebot look out for these elements and parse the contents.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@context&amp;quot;: &amp;quot;https://schema.org&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, a JSON object is initialised and the property &lt;code&gt;@context&lt;/code&gt; is set to &lt;code&gt;https://schema.org&lt;/code&gt;. In JSON-LD, the structure of data is determined by assigning the appropriate context. Web crawlers are standardised on &lt;a href=&quot;https://schema.org&quot;&gt;Schema.org,&lt;/a&gt; which defines all the valid key-value pairs for the JSON.&lt;/p&gt;
&lt;p&gt;Now that we&amp;#39;ve defined the schema our JSON-LD is following, we can describe our webpage!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@graph&amp;quot;: [
    {
      &amp;quot;@type&amp;quot;: &amp;quot;WebSite&amp;quot;,
      &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;
    }
    // Insert more nodes here.
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A JSON-LD document can be thought of as a labelled, directed graph, stored under &lt;code&gt;@graph&lt;/code&gt;. The graph contains multiple nodes, connected to each other with directed arcs.&lt;/p&gt;
&lt;p&gt;Nodes have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@type&lt;/code&gt; - Describes what the node is, e.g. &lt;code&gt;WebSite&lt;/code&gt; or &lt;code&gt;SoftwareApplication&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@id&lt;/code&gt; - A unique identifier for the node, typically a URL with a unique hash value at the end&lt;/li&gt;
&lt;li&gt;Properties - Key/Value pairs that describe the attributes of the node&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the example above, the type is &lt;code&gt;WebSite&lt;/code&gt;, the ID is &lt;code&gt;https://hawksley.dev/#website&lt;/code&gt;, and it has two properties, &lt;code&gt;url&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Web crawlers can merge the properties of a node across multiple pages, as long as they share an ID. However, scrapers that only read one page - such as LLMs - will not merge the properties. When JSON-LD is reused across pages, striking this balance is important to keep in mind. It is best practice for the ID to be a URL followed by a hash, such as &lt;code&gt;#website&lt;/code&gt;, that uniquely identifies the node.&lt;/p&gt;
&lt;p&gt;Although the Schema.org context defines many types of nodes, this guide will only be covering nodes that have noticeable SEO impact. If you&amp;#39;re interested in more, look up the semantic web - it&amp;#39;s a fun rabbit hole.&lt;/p&gt;
&lt;p&gt;Let&amp;#39;s move on to which nodes each page on our site should include. For each type, I&amp;#39;ve included the JSON-LD from this site, so you can copy-paste and edit it to fit your own.&lt;/p&gt;
&lt;h2&gt;WebSite&lt;/h2&gt;
&lt;p&gt;You&amp;#39;ve seen an extract of &lt;code&gt;WebSite&lt;/code&gt; earlier! Now here&amp;#39;s the full version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;WebSite&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;,
  &amp;quot;alternateName&amp;quot;: [&amp;quot;hawksley.dev&amp;quot;, &amp;quot;Hawksley&amp;quot;],
  &amp;quot;description&amp;quot;: &amp;quot;The personal site and technical blog of Ethan Hawksley, a UK-based CS student with a focus on systems programming, low-level computing, and cybersecurity.&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;publisher&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;image&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;ImageObject&amp;quot;,
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website-image&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/logo-square.png&amp;quot;,
    &amp;quot;caption&amp;quot;: &amp;quot;Ethan Hawksley Logo&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WebSite&lt;/code&gt; explains the metadata about the site. It gives crawlers hints on how to display your site.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/json-ld-explained-for-personal-websites/ethan-hawksley-sitename.png&quot; alt=&quot;Google search results showing hawksley.dev, with the site&amp;#39;s name &amp;quot;Ethan Hawksley&amp;quot; highlighted&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here, you can see that Google has interpreted the name field as representative of the domain and is labelling the result appropriately.&lt;/p&gt;
&lt;p&gt;Although &lt;code&gt;WebSite&lt;/code&gt; applies to every page, you don&amp;#39;t need to include the full version of it on every page. The root page of the domain should be fully detailed, but it is perfectly acceptable for other pages to have a slimmed-down version:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;WebSite&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives sufficient context to single-page crawlers so they correctly name the site, but they don&amp;#39;t need the full details.&lt;/p&gt;
&lt;h2&gt;WebPage&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WebPage&lt;/code&gt; describes the current page, but it&amp;#39;s important to distinguish it from other types like &lt;code&gt;BlogPosting&lt;/code&gt; (covered later). &lt;code&gt;WebPage&lt;/code&gt; represents the physical page itself, the HTML. It contains the content of the page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;WebPage&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/hack-club-campfire/#webpage&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/blog/hack-club-campfire/&amp;quot;,
  &amp;quot;isPartOf&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;
  },
  &amp;quot;name&amp;quot;: &amp;quot;Winning the Hack Club Campfire Hackathon&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;breadcrumb&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/hack-club-campfire/#breadcrumb&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are more specific subtypes of &lt;code&gt;WebPage&lt;/code&gt;. In this post, I&amp;#39;ll cover &lt;code&gt;ProfilePage&lt;/code&gt; and &lt;code&gt;CollectionPage&lt;/code&gt;. You can find less common ones at the bottom of &lt;a href=&quot;https://schema.org/WebPage&quot;&gt;Schema.org&amp;#39;s definition for WebPage.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Person&lt;/h2&gt;
&lt;p&gt;Another node that every page on a personal website should have is &lt;code&gt;Person&lt;/code&gt;. It describes who you are, which Google uses as part of their content quality metric. Increasingly, LLM crawlers are also using it to decide who to cite in their answers.&lt;/p&gt;
&lt;p&gt;Unlike &lt;code&gt;WebSite&lt;/code&gt;, it is important enough context that you should include it on all of your site&amp;#39;s pages.&lt;/p&gt;
&lt;details&gt;
  &lt;summary&gt;Warning - Quite Long!&lt;/summary&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;Person&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;,
  &amp;quot;alternateName&amp;quot;: &amp;quot;ethanhawksley&amp;quot;,
  &amp;quot;givenName&amp;quot;: &amp;quot;Ethan&amp;quot;,
  &amp;quot;familyName&amp;quot;: &amp;quot;Hawksley&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Long Description&amp;quot;,
  &amp;quot;disambiguatingDescription&amp;quot;: &amp;quot;Shorter Description&amp;quot;,
  &amp;quot;jobTitle&amp;quot;: &amp;quot;Computer Science Student&amp;quot;,
  &amp;quot;knowsLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;knowsAbout&amp;quot;: [
    // Keywords
  ],
  &amp;quot;nationality&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;Country&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;United Kingdom&amp;quot;
  },
  &amp;quot;homeLocation&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;Place&amp;quot;,
    &amp;quot;address&amp;quot;: {
      &amp;quot;@type&amp;quot;: &amp;quot;PostalAddress&amp;quot;,
      &amp;quot;addressCountry&amp;quot;: &amp;quot;GB&amp;quot;
    }
  },
  &amp;quot;affiliation&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;HighSchool&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;https://www.alcestergs.co.uk&amp;quot;,
    &amp;quot;name&amp;quot;: &amp;quot;Alcester Grammar School&amp;quot;,
    &amp;quot;sameAs&amp;quot;: [
      &amp;quot;https://www.wikidata.org/wiki/Q4713005&amp;quot;,
      &amp;quot;https://en.wikipedia.org/wiki/Alcester_Grammar_School&amp;quot;
    ]
  },
  &amp;quot;alumniOf&amp;quot;: [
    {
      &amp;quot;@type&amp;quot;: &amp;quot;HighSchool&amp;quot;,
      &amp;quot;url&amp;quot;: &amp;quot;https://www.brookeweston.org&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;Brooke Weston Academy&amp;quot;,
      &amp;quot;sameAs&amp;quot;: [
        &amp;quot;https://www.wikidata.org/wiki/Q4974495&amp;quot;,
        &amp;quot;https://en.wikipedia.org/wiki/Brooke_Weston_Academy&amp;quot;
      ]
    }
  ],
  &amp;quot;image&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;ImageObject&amp;quot;,
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person-image&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/ethan-hawksley.png&amp;quot;,
    &amp;quot;caption&amp;quot;: &amp;quot;Ethan Hawksley&amp;quot;,
    &amp;quot;width&amp;quot;: 1536,
    &amp;quot;height&amp;quot;: 1536
  },
  &amp;quot;sameAs&amp;quot;: [
    &amp;quot;https://github.com/ethan-hawksley&amp;quot;,
    &amp;quot;https://www.linkedin.com/in/ethanhawksley&amp;quot;,
    &amp;quot;https://lobste.rs/~ethanhawksley&amp;quot;,
    &amp;quot;https://news.ycombinator.com/user?id=ethanhawksley&amp;quot;
    // etc. etc.
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;&lt;p&gt;Phew! There&amp;#39;s plenty of properties for &lt;code&gt;Person&lt;/code&gt;. I find that it helps to be more descriptive, rather than less, when it comes to filling it out. Let’s look at the most important properties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;url&lt;/code&gt; - Points to your root page, anchoring the node.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;givenName&lt;/code&gt;, &lt;code&gt;familyName&lt;/code&gt; - Clearly describes your name.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;image&lt;/code&gt; - Preferably a photo of you, or a logo you are affiliated with. Connects you to a canonical image of you.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sameAs&lt;/code&gt; - Immensely useful for disambiguation, especially if you have a common name. It cleanly informs crawlers what your other profiles are, letting them build a knowledge graph representation of you across multiple pages. At the time of writing, &lt;a href=&quot;https://www.google.com/search?kgmid=/g/11m62cgdtf&quot;&gt;/g/11m62cgdtf&lt;/a&gt; is my Google knowledge graph ID.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The other properties of &lt;code&gt;Person&lt;/code&gt; are useful for adding more detail, but aren&amp;#39;t strictly necessary. You can trim them if you wish with only minor impact.&lt;/p&gt;
&lt;h2&gt;ProfilePage&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;ProfilePage&lt;/code&gt;, as you may expect, describes a page on the site about a person. For instance, I use this node on my home page, as that&amp;#39;s where I talk about myself. On your site, putting it on an about page could be more appropriate.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;ProfilePage&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#webpage&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
  &amp;quot;isPartOf&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;
  },
  &amp;quot;name&amp;quot;: &amp;quot;About Ethan Hawksley&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;dateCreated&amp;quot;: &amp;quot;2024-09-10T00:00:00.000Z&amp;quot;,
  &amp;quot;dateModified&amp;quot;: &amp;quot;2026-05-17T00:00:00.000Z&amp;quot;,
  &amp;quot;mainEntity&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It&amp;#39;s important to use &lt;code&gt;isPartOf&lt;/code&gt; to link it to your broader &lt;code&gt;WebSite&lt;/code&gt; node, to create a relationship between the two nodes. The same applies for &lt;code&gt;mainEntity&lt;/code&gt;, it lets crawlers know who the page is about. Including &lt;code&gt;dateCreated&lt;/code&gt; and &lt;code&gt;dateModified&lt;/code&gt; is a good freshness signal for crawlers, but if your site doesn&amp;#39;t have them readily available, don&amp;#39;t worry too much about it.&lt;/p&gt;
&lt;h2&gt;SoftwareApplication&lt;/h2&gt;
&lt;p&gt;If you are showcasing any software on your page, it&amp;#39;s a good idea to include a &lt;code&gt;SoftwareApplication&lt;/code&gt; node to describe the metadata about it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;SoftwareApplication&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#project-yt-play&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://crates.io/crates/yt-play&amp;quot;,
  &amp;quot;name&amp;quot;: &amp;quot;yt-play&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;A CLI utility written in Rust that synchronises YouTube playlists to local directories.&amp;quot;,
  &amp;quot;applicationCategory&amp;quot;: &amp;quot;MultimediaApplication&amp;quot;,
  &amp;quot;operatingSystem&amp;quot;: &amp;quot;All&amp;quot;,
  &amp;quot;creator&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;sameAs&amp;quot;: [&amp;quot;https://github.com/ethan-hawksley/yt-play&amp;quot;],
  &amp;quot;offers&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;Offer&amp;quot;,
    &amp;quot;price&amp;quot;: 0,
    &amp;quot;priceCurrency&amp;quot;: &amp;quot;GBP&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to be more specific than &lt;code&gt;SoftwareApplication&lt;/code&gt;, other valid types for this node are &lt;code&gt;MobileApplication&lt;/code&gt;, &lt;code&gt;WebApplication&lt;/code&gt;, and &lt;code&gt;VideoGame&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;url&lt;/code&gt; property should be a link to where the project is deployed, e.g. crates.io. &lt;code&gt;sameAs&lt;/code&gt; is for any other pages associated with the project, like its source code repository.&lt;/p&gt;
&lt;p&gt;There are lots of valid values for &lt;code&gt;applicationCategory&lt;/code&gt;, you can find a list on &lt;a href=&quot;https://developers.google.com/search/docs/appearance/structured-data/software-app&quot;&gt;Google&amp;#39;s definition for SoftwareApplication.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Even if your project is FOSS, include &lt;code&gt;offers&lt;/code&gt; but make sure to set the price to 0.&lt;/p&gt;
&lt;h2&gt;BreadcrumbList&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BreadcrumbList&lt;/code&gt; is widely useful and should be included on all pages aside from the root page. It is used to describe the categorisation of a page, which isn&amp;#39;t necessarily the actual path to the page.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;BreadcrumbList&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/hack-club-shipwrecked/#breadcrumb&amp;quot;,
  &amp;quot;itemListElement&amp;quot;: [
    {
      &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;,
      &amp;quot;item&amp;quot;: &amp;quot;https://hawksley.dev/&amp;quot;,
      &amp;quot;position&amp;quot;: 1,
      &amp;quot;name&amp;quot;: &amp;quot;Home&amp;quot;
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;,
      &amp;quot;item&amp;quot;: &amp;quot;https://hawksley.dev/blog/&amp;quot;,
      &amp;quot;position&amp;quot;: 2,
      &amp;quot;name&amp;quot;: &amp;quot;Blog&amp;quot;
    },
    {
      &amp;quot;@type&amp;quot;: &amp;quot;ListItem&amp;quot;,
      &amp;quot;item&amp;quot;: &amp;quot;https://hawksley.dev/blog/hack-club-shipwrecked/&amp;quot;,
      &amp;quot;position&amp;quot;: 3,
      &amp;quot;name&amp;quot;: &amp;quot;The Shipwrecked Hackathon by Hack Club&amp;quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;BreadcrumbList&lt;/code&gt; describes the path of a page. By including one, you can control how search engines represent the path of a specific page.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/json-ld-explained-for-personal-websites/hawksley-dev-breadcrumbs.png&quot; alt=&quot;Google search results for The Shipwrecked Hackathon by Hack Club, with the breadcrumbs highlighted&quot;&gt;&lt;/p&gt;
&lt;p&gt;Here, the search result for my blog post contains the path &lt;code&gt;https://hawksley.dev › Blog&lt;/code&gt;. If your site already uses short paths, this node is a minor gain and can be omitted. However, if your paths are longer, &lt;code&gt;BreadcrumbList&lt;/code&gt; is useful for shortening them.&lt;/p&gt;
&lt;h2&gt;CollectionPage&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;CollectionPage&lt;/code&gt; node is a subtype of &lt;code&gt;WebPage&lt;/code&gt;, usable for pages that primarily contain lists. For example, my /elsewhere/ page lists all my other profiles, and /blog/ lists all my blog posts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;CollectionPage&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/elsewhere/#webpage&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/elsewhere/&amp;quot;,
  &amp;quot;isPartOf&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;
  },
  &amp;quot;name&amp;quot;: &amp;quot;Elsewhere&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Online profiles of Ethan Hawksley, a UK-based CS student. Links to his development, social media, technical writing, and security accounts.&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;about&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;breadcrumb&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/elsewhere/#breadcrumb&amp;quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You&amp;#39;ve met most of these properties already, so they are largely self-explanatory. Make sure you link &lt;code&gt;breadcrumb&lt;/code&gt; to the correct &lt;code&gt;BreadcrumbList&lt;/code&gt;! It needs to be the one on the current page for it to make any sense.&lt;/p&gt;
&lt;h2&gt;Blog&lt;/h2&gt;
&lt;p&gt;You should add the &lt;code&gt;Blog&lt;/code&gt; node to your blog&amp;#39;s index or home page. It acts as a stepping stone between your &lt;code&gt;WebSite&lt;/code&gt; and the individual blog posts you publish.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;Blog&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/#blog&amp;quot;,
  &amp;quot;isPartOf&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#website&amp;quot;
  },
  &amp;quot;mainEntityOfPage&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/#webpage&amp;quot;
  },
  &amp;quot;name&amp;quot;: &amp;quot;Ethan Hawksley&amp;#39;s Blog&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Technical blog of Ethan Hawksley, a UK-based CS student. Articles on systems programming, low-level computing, cybersecurity, and computer science.&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;dateModified&amp;quot;: &amp;quot;2026-05-17T00:00:00.000Z&amp;quot;,
  &amp;quot;publisher&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;license&amp;quot;: &amp;quot;https://creativecommons.org/licenses/by/4.0/&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;dateModified&lt;/code&gt; is a good freshness signal, but if you don&amp;#39;t have it handy, don&amp;#39;t worry. Including &lt;code&gt;license&lt;/code&gt; lets crawlers know under what circumstances they can use your prose.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/json-ld-explained-for-personal-websites/jsonld-publisher-organization-person.png&quot; alt=&quot;Definition of publisher property, where available types include Organization and Person&quot;&gt;&lt;/p&gt;
&lt;p&gt;If you&amp;#39;ve done prior research into JSON-LD, you may be surprised that the &lt;code&gt;publisher&lt;/code&gt; property is set to be a &lt;code&gt;Person&lt;/code&gt;, not an &lt;code&gt;Organization&lt;/code&gt;. Although it used to require one, Google&amp;#39;s documentation has since been relaxed and &lt;code&gt;Person&lt;/code&gt; is entirely valid too, and arguably more accurate for a personal website.&lt;/p&gt;
&lt;h2&gt;BlogPosting&lt;/h2&gt;
&lt;p&gt;The last node we&amp;#39;ll be covering is &lt;code&gt;BlogPosting&lt;/code&gt;. It should be included on all published blog posts, providing added information to crawlers so they can more accurately represent them, including both more accurate placement and richer details in search results.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;@type&amp;quot;: &amp;quot;BlogPosting&amp;quot;,
  &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/need-for-post-quantum-cryptography/#blogposting&amp;quot;,
  &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/blog/need-for-post-quantum-cryptography/&amp;quot;,
  &amp;quot;mainEntityOfPage&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/need-for-post-quantum-cryptography/#webpage&amp;quot;
  },
  &amp;quot;isPartOf&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/#blog&amp;quot;
  },
  &amp;quot;headline&amp;quot;: &amp;quot;The Need for Post-Quantum Cryptography&amp;quot;,
  &amp;quot;description&amp;quot;: &amp;quot;Quantum computers are closer to breaking RSA and ECC than we thought. Learn what post-quantum cryptography is and how to start migrating.&amp;quot;,
  &amp;quot;articleSection&amp;quot;: &amp;quot;cybersecurity&amp;quot;,
  &amp;quot;keywords&amp;quot;: &amp;quot;cybersecurity, quantum&amp;quot;,
  &amp;quot;inLanguage&amp;quot;: &amp;quot;en-GB&amp;quot;,
  &amp;quot;datePublished&amp;quot;: &amp;quot;2026-04-13T00:00:00.000Z&amp;quot;,
  &amp;quot;dateModified&amp;quot;: &amp;quot;2026-04-17T00:00:00.000Z&amp;quot;,
  &amp;quot;author&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;publisher&amp;quot;: {
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/#person&amp;quot;
  },
  &amp;quot;image&amp;quot;: {
    &amp;quot;@type&amp;quot;: &amp;quot;ImageObject&amp;quot;,
    &amp;quot;@id&amp;quot;: &amp;quot;https://hawksley.dev/blog/need-for-post-quantum-cryptography/#blogposting-image&amp;quot;,
    &amp;quot;url&amp;quot;: &amp;quot;https://hawksley.dev/og/blog/need-for-post-quantum-cryptography.jpg&amp;quot;,
    &amp;quot;width&amp;quot;: 1200,
    &amp;quot;height&amp;quot;: 630
  },
  &amp;quot;license&amp;quot;: &amp;quot;https://creativecommons.org/licenses/by/4.0/&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since this is a personal site, it is alright for &lt;code&gt;author&lt;/code&gt; and &lt;code&gt;publisher&lt;/code&gt; to both point towards the same &lt;code&gt;Person&lt;/code&gt; node. The &lt;code&gt;image&lt;/code&gt; property should mirror the OG image that the post already uses for link previews.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Congrats, that&amp;#39;s all the JSON-LD a personal site needs! I&amp;#39;ve structured this post to make it as easy as possible for you to copy-paste and implement into your own personal site. Even if you run a static site without a build step, you can still benefit from adding at a minimum &lt;code&gt;WebSite&lt;/code&gt;, &lt;code&gt;ProfilePage&lt;/code&gt;, and &lt;code&gt;Person&lt;/code&gt; to the root page of your site.&lt;/p&gt;
&lt;p&gt;If you have any questions, you can get in touch via email, and I&amp;#39;ll do my best to help.&lt;/p&gt;
</content:encoded><category>seo</category><category>web</category></item><item><title>Mitigating Ongoing Registry Supply Chain Attacks</title><link>https://hawksley.dev/blog/supply-chain-attacks/</link><guid isPermaLink="true">https://hawksley.dev/blog/supply-chain-attacks/</guid><description>They are only becoming more frequent and more damaging.</description><pubDate>Sun, 17 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Supply chain attacks are a growing threat to software registries.&lt;/p&gt;
&lt;p&gt;One of the first major typosquatting attacks on npm took place in 2017 when the user &amp;quot;hacktask&amp;quot; &lt;a href=&quot;https://blog.npmjs.org/post/163723642530/crossenv-malware-on-the-npm-registry&quot;&gt;published a series of packages&lt;/a&gt; with similar names to popular libraries (e.g., crossenv vs cross-env), misleading people into installing them.&lt;/p&gt;
&lt;p&gt;Then the first time an official package was compromised was in 2018, when the account of an &lt;code&gt;eslint-scope&lt;/code&gt; maintainer &lt;a href=&quot;https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes/&quot;&gt;was hijacked&lt;/a&gt; and used to publish malicious packages. These packages would exfiltrate the contents of developers&amp;#39; &lt;code&gt;.npmrc&lt;/code&gt; files, often including their keys used for publishing new npm packages. This marked a turning point when attackers began harvesting credentials at scale, to fuel further attacks.&lt;/p&gt;
&lt;p&gt;The recent attack - named &lt;a href=&quot;https://www.akamai.com/blog/security-research/mini-shai-hulud-worm-returns-goes-public&quot;&gt;Mini Shai-Hulud&lt;/a&gt; as a homage to the Dune sandworms - is a symptom of how broken the security of these registries is. It scans your system for API keys, creates public GitHub repositories containing said API keys, and abuses credentials to upload itself to other libraries, spreading its influence.&lt;/p&gt;
&lt;p&gt;Software developers are large targets for malicious actors due to their unique position as both users and distributors. They can easily be compromised by a single library and spread the infection rapidly to all downstream users. Paired with keys to expensive cloud billing accounts and browser token-stealers, it&amp;#39;s no wonder developers face these attacks so often.&lt;/p&gt;
&lt;p&gt;Not all registries are at equal risk, npm is the worst offender by far. A recent report shows it contained &lt;a href=&quot;https://www.reversinglabs.com/blog/sscs-report-2026-takeaways&quot;&gt;90% of all detected malware&lt;/a&gt; across a collection of registries. PyPI and crates.io both contain some malware, but their scale is dwarfed.&lt;/p&gt;
&lt;h2&gt;Mitigations&lt;/h2&gt;
&lt;p&gt;Despite the dangers associated with supply chain attacks, they can be mitigated. The easiest method is by setting a minimum-age on new packages. Most malicious packages from hijacked libraries last no longer than a few hours, so by setting an age-requirement of a week, the vast majority of threats can be foiled.&lt;/p&gt;
&lt;p&gt;For npm, set the contents of &lt;code&gt;~/.npmrc&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;min-release-age=7
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For pnpm, run the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm config set minimumReleaseAge 10080 --global
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Bun, set the contents of &lt;code&gt;~/.bunfig.toml&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[install]
minimumReleaseAge = 604800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Yarn, run the command:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;yarn config set --home npmMinimalAgeGate 10080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For uv, set the contents of &lt;code&gt;~/.config/uv/uv.toml&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;exclude-newer = &amp;quot;7 days&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For pip, set the contents of &lt;code&gt;~/.config/pip/pip.conf&lt;/code&gt; to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[install]
uploaded-prior-to = P7D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cargo has a minimum age feature as an RFC, but as of the time of writing it is not implemented in the client yet. Go has no such plans, nor do Maven or Gradle.&lt;/p&gt;
&lt;p&gt;This mitigation is not without drawbacks. It&amp;#39;s worth considering that delaying such installations by a week also delays any critical security patches, unless otherwise overridden. It&amp;#39;s worth weighing up whether it is worth the trade-off.&lt;/p&gt;
&lt;h2&gt;Other Considerations&lt;/h2&gt;
&lt;p&gt;Aside from a minimum-age, there are a few other key changes worth implementing.&lt;/p&gt;
&lt;p&gt;Firstly, ensure you have 2FA set up for all accounts that support it. It greatly limits the potential blast radius if you become compromised. I personally recommend &lt;a href=&quot;https://ente.com/auth/&quot;&gt;Ente Auth&lt;/a&gt; - it&amp;#39;s free, cross-platform, and supports optional E2EE cloud backups.&lt;/p&gt;
&lt;p&gt;Secondly, if you are not using &lt;a href=&quot;https://jonathan.bergknoff.com/journal/always-pin-your-versions/&quot;&gt;pinned versions&lt;/a&gt; or &lt;a href=&quot;https://dev.to/wilsonwangdev/lock-files-and-package-manager-migration-a-practical-risk-analysis-2ejn&quot;&gt;lock-files,&lt;/a&gt; you should be. They ensure that installed software versions will only change when explicitly updated. This gives you greater control over how and when you want to update, limiting your exposure to supply chain attacks.&lt;/p&gt;
&lt;p&gt;Lastly, if your language supports &lt;a href=&quot;https://docs.npmjs.com/cli/v11/using-npm/scripts&quot;&gt;post-install scripts,&lt;/a&gt; it&amp;#39;s worth disabling them unless necessary. Especially in the JS ecosystem, post-install scripts are often abused to execute payloads immediately before you have time to react. In most circumstances, the feature is just an unnecessary attack surface.&lt;/p&gt;
&lt;p&gt;Even with these mitigations, no project is immune. However, with enough common sense and precautions, you should be able to keep reasonably safe.&lt;/p&gt;
</content:encoded><category>cybersecurity</category><category>packages</category></item><item><title>The Need for Post-Quantum Cryptography</title><link>https://hawksley.dev/blog/need-for-post-quantum-cryptography/</link><guid isPermaLink="true">https://hawksley.dev/blog/need-for-post-quantum-cryptography/</guid><description>Quantum computers are becoming increasingly realistic. We need to be ready.</description><pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The threat quantum computers pose to encryption is growing faster than many expected. Historically, the internet has relied on algorithms like &lt;a href=&quot;https://brilliant.org/wiki/rsa-encryption/&quot;&gt;RSA encryption&lt;/a&gt; and &lt;a href=&quot;https://blog.cloudflare.com/a-relatively-easy-to-understand-primer-on-elliptic-curve-cryptography/&quot;&gt;Elliptic Curve Cryptography (ECC)&lt;/a&gt; to exchange encryption keys and verify identities across the web. These algorithms are in danger of being entirely broken by the new capabilities of quantum computers, leaving our communications insecure. Modern breakthroughs in research are bringing us closer to this becoming a reality than we expected.&lt;/p&gt;
&lt;p&gt;Quantum computers can be thought of as computers with fundamentally unique capabilities. Instead of using bits as memory, they use &lt;a href=&quot;https://www.ibm.com/think/topics/qubit&quot;&gt;qubits.&lt;/a&gt; Quantum computers can run specialised &amp;quot;quantum algorithms&amp;quot; which, in some circumstances, are much faster than conventional algorithms[^1]. RSA and ECC seemed secure when we designed them, but as we research more about quantum computers, we realise that they are not secure at all. To resolve this, we have now designed post-quantum algorithms capable of securing data even after quantum computers are accessible. NIST &lt;a href=&quot;https://csrc.nist.gov/pubs/fips/203/final&quot;&gt;finalised its post-quantum standards&lt;/a&gt; in 2024, introducing new post-quantum algorithms such as ML-KEM to replace the old vulnerable ones.&lt;/p&gt;
&lt;p&gt;Shor&amp;#39;s algorithm - the algorithm responsible for factorising the integers used in RSA encryption - has &lt;a href=&quot;https://arxiv.org/html/2603.28627v1&quot;&gt;been improved&lt;/a&gt; and requires far fewer qubits than before. Google has also made quantum algorithms used for breaking ECC &lt;a href=&quot;https://arxiv.org/abs/2603.28846&quot;&gt;significantly more efficient&lt;/a&gt;. Major tech companies like Google, Microsoft, and IBM are competing to build bigger and faster quantum computers, bringing us closer to these quantum algorithms becoming possible. With these developments, the advent of quantum computing is closer than we had once expected. In response, Google has announced a &lt;a href=&quot;https://blog.google/innovation-and-ai/technology/safety-security/cryptography-migration-timeline/&quot;&gt;2029 timeline&lt;/a&gt; for using post-quantum cryptography, and &lt;a href=&quot;https://blog.cloudflare.com/post-quantum-roadmap/&quot;&gt;Cloudflare has followed suit&lt;/a&gt; with their global network. It won&amp;#39;t be long until other major providers follow.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/need-for-post-quantum-cryptography/cisa-post-quantum.png&quot; alt=&quot;Cloudflare Radar showing cisa.gov not supporting post-quantum cryptography&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the course of researching this, I discovered that America&amp;#39;s Cybersecurity and Infrastructure Security Agency surprisingly still doesn&amp;#39;t support post-quantum cryptography. If you want to check some websites yourself, &lt;a href=&quot;https://radar.cloudflare.com/post-quantum&quot;&gt;Cloudflare offers a free tool&lt;/a&gt; where you can enter any URL and check.&lt;/p&gt;
&lt;h2&gt;The Risks&lt;/h2&gt;
&lt;p&gt;Adversaries aren&amp;#39;t waiting, even though experts in the field predict that quantum computers capable of breaking RSA are &lt;a href=&quot;https://globalriskinstitute.org/publication/quantum-threat-timeline-report-2025b/&quot;&gt;likely to be built in 10-15 years.&lt;/a&gt; They are already harvesting huge quantities of our encrypted communications - known as &lt;a href=&quot;https://www.federalreserve.gov/econres/feds/files/2025093pap.pdf&quot;&gt;Harvest Now, Decrypt Later&lt;/a&gt; (HNDL) attacks - knowing they will be able to decrypt them later.&lt;/p&gt;
&lt;p&gt;Adversaries aren&amp;#39;t selective, they are collecting all the data they can get their hands on. Once quantum computers arrive, they&amp;#39;ll decide what&amp;#39;s worth decrypting. The question is simply whether your data will still matter in ten years.&lt;/p&gt;
&lt;p&gt;On a personal level, private messages, account records, &lt;a href=&quot;https://pmc.ncbi.nlm.nih.gov/articles/PMC12540643/&quot;&gt;medical records,&lt;/a&gt; and biometrics are in danger of being exposed. Some sensitive data, like passport pages and credit card details, will likely have expired by then, so there isn&amp;#39;t much risk for those.&lt;/p&gt;
&lt;p&gt;For companies, trade secrets, source code, internal logs, and customer data pose great risk. Exposure of client information could lead to &lt;a href=&quot;https://gowlingwlg.com/en/insights-resources/articles/2025/data-protection-in-a-quantum-enabled-future&quot;&gt;legal fines,&lt;/a&gt; and trade secrets risk losing your competitive advantage.&lt;/p&gt;
&lt;p&gt;With governments, however, the stakes are highest. Foreign intelligence, real identities of agents in the field, military plans, and diplomatic communications are severe threats to &lt;a href=&quot;https://www.ncsc.gov.uk/blog-post/setting-direction-uk-migration-to-pqc&quot;&gt;national security.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;No matter the scale you operate at, the transition needs to happen before quantum hardware catches up. That window is narrowing.&lt;/p&gt;
&lt;p&gt;[^1]: If you&amp;#39;re interested in more details about how quantum computing works on a fundamental level, I highly recommend the &lt;a href=&quot;https://quantum.country&quot;&gt;quantum.country&lt;/a&gt; essay, which goes into far greater depth.&lt;/p&gt;
</content:encoded><category>cybersecurity</category><category>quantum</category></item><item><title>Winning the Hack Club Campfire Hackathon</title><link>https://hawksley.dev/blog/hack-club-campfire/</link><guid isPermaLink="true">https://hawksley.dev/blog/hack-club-campfire/</guid><description>A high-intensity 12-hour collaborative sprint.</description><pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As leader of ComSoc at my school, I guide the Year 12 and 13 students and keep on the lookout for any new opportunities we can take part in.&lt;/p&gt;
&lt;p&gt;A few months ago, we practised for the &lt;a href=&quot;https://olympiad.org.uk/&quot;&gt;British Informatics Olympiad,&lt;/a&gt; a challenging set of three questions that required us to design accurate algorithms to solve. Over a short period, I had helped transform a group of enthusiastic Year 12 students into confident programmers capable of breaking down and solving complex problems.&lt;/p&gt;
&lt;p&gt;Whilst looking for new events, I found &lt;a href=&quot;https://campfire.hackclub.com/&quot;&gt;Campfire,&lt;/a&gt; a team-based hackathon by Hack Club, where over the span of 12 hours, you design, create, and present a game, and the best teams receive prizes! Having previously competed in &lt;a href=&quot;https://daydream.hackclub.com/&quot;&gt;Daydream&lt;/a&gt; - a very similar game jam - I knew that this would be a lot of fun. After getting the members of ComSoc to split into teams and sign up, we were off to the races.&lt;/p&gt;
&lt;h2&gt;Preparing for Campfire&lt;/h2&gt;
&lt;p&gt;Nowadays, there&amp;#39;s plenty of choice for how to produce and release a game. The event was restricted to only game engines that could be exported to run on the web. After some thought, I chose to use &lt;a href=&quot;https://godotengine.org/&quot;&gt;Godot&lt;/a&gt; as its beginner-friendly nature makes it the perfect candidate for teaching.&lt;/p&gt;
&lt;p&gt;The members of my society needed some teaching, so after struggling through the pain that is Git credentials on Windows, I had them all set up and installed with the necessary software. After I ran a session on creating a simple platformer to bring them up to speed, I knew they were ready.&lt;/p&gt;
&lt;h2&gt;The Big Day&lt;/h2&gt;
&lt;p&gt;On the car journey over, I learned that the surprise theme was &amp;quot;Beneath the Surface&amp;quot;, which started to give me plenty of ideas.&lt;/p&gt;
&lt;p&gt;Once we all arrived at the location, I met up with my teammates. It took a little brainstorming, but we came up with the idea for our game. Inspired by the incremental games you often see in ads or see others playing, our game was to be based around a cat that goes fishing deeper and deeper into the ocean. Any caught fish could be sold to buy upgrades, allowing for even deeper levels to be reached. We named it &amp;quot;Cat-Fishing Frenzy&amp;quot;.&lt;/p&gt;
&lt;h2&gt;Developing the Game&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-campfire/cat-fishing-frenzy.png&quot; alt=&quot;Cat-Fishing Frenzy gameplay&quot;&gt;&lt;/p&gt;
&lt;p&gt;We began the event by first creating the assets we would need. I used &lt;a href=&quot;https://www.aseprite.org/&quot;&gt;Aseprite&lt;/a&gt; to design the pixel art for many of the fish that would swim in the sea.&lt;/p&gt;
&lt;p&gt;Next, we laid the foundations. The main scene was created and populated with the cat, rod, and boat. After adding the fishhook, Ryley discovered a neat way to create the fishing line by just moving a point on a Godot Line2D.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gdscript&quot;&gt;func _process(_delta: float) -&amp;gt; void:
	set_point_position(1, to_local(node.global_position))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ryley and I worked on fish spawning, whilst Sam implemented a currency and shop system. With each of us working on largely different areas of the codebase, Git merge conflicts were easy to solve and very infrequent.&lt;/p&gt;
&lt;p&gt;After not too long, we had created the skeleton of a fully functional game! With plenty of time to spare, we introduced a couple of smaller mechanics, and the game received many balancing tweaks. After some polishing, the time was up, just in time for the pizza that the organisers ordered.&lt;/p&gt;
&lt;p&gt;We presented the game to the other competitors, and then came a voting round. After a suspenseful wait, the results were in, with &lt;a href=&quot;https://refractfoundation.org/events/campfire&quot;&gt;our game taking first place!&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Retrospective&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-campfire/group-photo.webp&quot; alt=&quot;Hack Club Campfire participants&quot;&gt;&lt;/p&gt;
&lt;p&gt;This hackathon was plenty of fun to attend, and winning it was the icing on top. Lots of nice prizes were dished out, but I really enjoyed getting to know the people I was working with better. Programming under strict time constraints was excellent practice for my efficiency and teamwork skills, and I look forward to further opportunities in the future.&lt;/p&gt;
&lt;p&gt;If you&amp;#39;re curious, you can play &lt;a href=&quot;https://ethanhawksley.itch.io/cat-fishing-frenzy&quot;&gt;Cat-Fishing Frenzy&lt;/a&gt; here on itch.io. You&amp;#39;ll need the arrow keys to move the hook, so no mobile compatibility at the time of writing.&lt;/p&gt;
</content:encoded><category>hack-club</category><category>hackathon</category></item><item><title>The Shipwrecked Hackathon by Hack Club</title><link>https://hawksley.dev/blog/hack-club-shipwrecked/</link><guid isPermaLink="true">https://hawksley.dev/blog/hack-club-shipwrecked/</guid><description>Shipwrecked was really fun, actually.</description><pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last April, I was minding my own business when I received a curious email in my inbox.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Hack Club is renting an island.&lt;/li&gt;
&lt;li&gt;We are hosting a hackathon.&lt;/li&gt;
&lt;li&gt;Anyone who ships 4 projects gets to come. Flight stipends available!&lt;/li&gt;
&lt;li&gt;Dates are August 8th - 11th. Boston, USA.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;I&amp;#39;d signed up for &lt;a href=&quot;https://hackclub.com&quot;&gt;Hack Club&lt;/a&gt; mailing lists in the past since they run a variety of coding activities. However, this email particularly stood out to me. I&amp;#39;ve always wanted to visit America, and it seemed like they would be offering me a free flight!&lt;/p&gt;
&lt;p&gt;Following the link led me to a sign-up page where I entered my details to register my interest. I spread the word about this event to the other coders I knew, and soon it reached the 5,000-signup threshold to be given the green light.&lt;/p&gt;
&lt;p&gt;Hack Club is a non-profit organisation that has received some &lt;a href=&quot;https://eu.burlingtonfreepress.com/story/news/2021/11/26/hack-club-shelburne-vt-grant-elon-musk-teens-coding/8552542002/&quot;&gt;generous donations&lt;/a&gt; in the past, but they still can&amp;#39;t afford to give away such a great trip for free. To earn my place at the event, I needed to pass The Bay.&lt;/p&gt;
&lt;h2&gt;The Bay&lt;/h2&gt;
&lt;p&gt;The Bay was an online platform designed by Hack Club specifically for this event. You tracked your time spent coding projects with their WakaTime server &lt;a href=&quot;https://hackatime.hackclub.com/&quot;&gt;Hackatime&lt;/a&gt; and submitted any completed projects for review by the Shipwrecked moderators.&lt;/p&gt;
&lt;p&gt;Approved projects granted you a number of &amp;quot;shells&amp;quot; relative to how many hours were spent creating the project. These shells could be spent on many different items from their shop, from Flipper Zeros to 3D printers. However, the grand prize was a ticket for the Shipwrecked hackathon in Boston. This cost the small sum of 60 hours total. Travel stipends were also available, at a conversion rate of 1 hour coding → $10 grant towards a flight.&lt;/p&gt;
&lt;p&gt;After checking Skyscanner to see return flights would charge an entire £400 (~$550), I checked the clauses for Shipwrecked and saw that if I programmed for 100 hours total, they would fully cover my flight regardless of how expensive.&lt;/p&gt;
&lt;p&gt;Now I had the 100-hour goal in mind, it was time to start programming.&lt;/p&gt;
&lt;h2&gt;Some Notable Projects&lt;/h2&gt;
&lt;h3&gt;Gravify&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/gravify-extension.jpg&quot; alt=&quot;Gravify extension in action&quot;&gt;&lt;/p&gt;
&lt;p&gt;I had found an interesting library called &lt;a href=&quot;https://github.com/bubkoo/html-to-image&quot;&gt;html-to-image&lt;/a&gt; that could convert DOM elements into images I could manipulate with JavaScript. While that library was sitting in the back of my mind, I stumbled across the &lt;a href=&quot;https://brm.io/matter-js/&quot;&gt;Matter.js&lt;/a&gt; physics simulation library. The library supported custom PNGs as input to the physics simulation, so I decided to combine these two libraries and recreate something very similar to &lt;a href=&quot;https://mrdoob.com/projects/chromeexperiments/google-gravity/&quot;&gt;Google Gravity.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Going through the &lt;a href=&quot;https://developer.mozilla.org/en-US/&quot;&gt;MDN Web Docs&lt;/a&gt; to learn how to build a browser extension was far smoother than I had expected. I was comfortable writing JavaScript, and not much boilerplate was required to hook into the Browser APIs. After not too long, &lt;a href=&quot;https://github.com/ethan-hawksley/gravify-extension&quot;&gt;Gravify&lt;/a&gt; was created.&lt;/p&gt;
&lt;h3&gt;Turing Machine Simulator&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/turing-machine-simulator.webp&quot; alt=&quot;Turing Machine Simulator&quot;&gt;&lt;/p&gt;
&lt;p&gt;After visits to Bletchley Park and diving into more theoretical computer science, I knew I wanted to build a Turing machine.&lt;/p&gt;
&lt;p&gt;Turing machines are a model for computing first devised by Alan Turing in 1936. They consist of an infinite roll of tape marked out into squares, a &amp;quot;head&amp;quot; that points to a single square at a time, and a set of states and instructions. Each cycle, the Turing machine reads the value of the tape underneath itself, compares it to the corresponding instruction, writes a new symbol onto the tape, and finally either moves left or right along the tape.&lt;/p&gt;
&lt;p&gt;Despite their limited nature, they can provably carry out any calculation and are often used for proving new theorems in computer science. I did some research and couldn&amp;#39;t find a simulator online that I was content with using, so I built my own.&lt;/p&gt;
&lt;p&gt;I was still studying a React course at the time, so I wasn&amp;#39;t fully confident building UI with JSX components. This led to a lot of repetitive code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const rightOption = document.createElement(&amp;#39;option&amp;#39;);
rightOption.value = &amp;#39;Right&amp;#39;;
rightOption.textContent = &amp;#39;Right&amp;#39;;
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking back at it, it is quite brittle and challenging to debug, but I can thank my understanding of JavaScript fundamentals for framework-less code like this.&lt;/p&gt;
&lt;h2&gt;Reaching America&lt;/h2&gt;
&lt;p&gt;As soon as I got the official go-ahead from the organisers, I booked a flight ready to set off for Boston.&lt;/p&gt;
&lt;p&gt;Coming alongside me were two friends who also qualified for the event. They had put in the hours to earn tickets, and we all came over together.&lt;/p&gt;
&lt;p&gt;The flight took a long eight hours, but after not too long we finally reached America.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/boston-pier.jpg&quot; alt=&quot;EDIC Pier&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once we arrived at the airport, we set off to EDIC Pier where we laid our picnic blankets and bought some local food. I got to know the other contestants who were there. Many were American, but surprising numbers were from all across the globe, with one even as far as Vietnam!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/ferry.jpg&quot; alt=&quot;Boston Harbour&quot;&gt;&lt;/p&gt;
&lt;p&gt;It didn&amp;#39;t take long until we began to set off towards Cathleen Stone Island. The ride was short but a great opportunity to chat with the other people there and take some photos. By the time we finally reached the island, we had already decided on the structure of our teams.&lt;/p&gt;
&lt;p&gt;Everyone scrambled to claim their beds, but as one of my projects had gone viral, I was given first pick and stayed in the executive suite with the other viral programmers. It was here that I met Daniel, another participant who managed to go viral, and formed a team with him and Oliver.&lt;/p&gt;
&lt;p&gt;By the end of the day, we were all invited down to the pavilion to learn about the first project we would be assigned. This hackathon had three projects that we would make in total, each themed around a certain idea. The first project was to design a method of communication that wouldn&amp;#39;t involve letters or numbers.&lt;/p&gt;
&lt;h2&gt;SymbolNotes&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/symbolnotes.png&quot; alt=&quot;SymbolNotes UI&quot;&gt;&lt;/p&gt;
&lt;p&gt;In my team, we decided to create an online peer-to-peer messaging app. Since I had prior experience with the PeerJS library, I suggested that we use that.&lt;/p&gt;
&lt;p&gt;As per hackathon tradition, the code we made was hacky, and the graphics were rough, but we managed to create a working solution. We released the source code for &lt;a href=&quot;https://github.com/Thesupernile/SymbolNotes&quot;&gt;SymbolNotes&lt;/a&gt; onto GitHub so others could examine and learn from it. We made it together over the course of a morning and submitted it onto The Bay.&lt;/p&gt;
&lt;h2&gt;Exploring The Island&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/map.jpg&quot; alt=&quot;Map of Cathleen Stone Island&quot;&gt;&lt;/p&gt;
&lt;p&gt;With our project complete, we spent the rest of the day exploring the island as a team. The executive suite and pavilion were both very close to the docks, but there was the entire rest of the island still to investigate.&lt;/p&gt;
&lt;p&gt;Alongside us were some Americans who knew the area much better than we did and told plenty of stories about what it was like living in America. As somebody who&amp;#39;d only set foot outside Europe once before, it was very refreshing to hear such different perspectives.&lt;/p&gt;
&lt;p&gt;Before long, however, it was back to the next challenge.&lt;/p&gt;
&lt;h2&gt;Connect Infinity&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/connect-infinity.png&quot; alt=&quot;Connect Infinity gameplay&quot;&gt;&lt;/p&gt;
&lt;p&gt;Our next challenge was to design a creative game that could be played online. We decided to put a spin on the classic game Connect 4 and make it real-time. To prevent it from ending extremely quickly, the game only ends once the entire board is full. Every 4-in-a-row gives a point, most points win.&lt;/p&gt;
&lt;p&gt;Now the team had got used to PeerJS, we decided to use it once again here as it seemed perfectly suited for the task. As a challenge, we designed the code so that lobbies of unlimited size were supported, instead of just groups of two.&lt;/p&gt;
&lt;p&gt;This took more time than the previous project due to its larger scope, but we worked as a team much more efficiently. After a few tireless hours, &lt;a href=&quot;https://github.com/ethan-hawksley/connect-infinity&quot;&gt;Connect Infinity&lt;/a&gt; was created just in time to present.&lt;/p&gt;
&lt;p&gt;We took laptops down to the pavilion and set them up running the game by the shore. The other participants all tried out the game, and it was surprisingly well received! We managed to win a limited-edition Shipwrecked shirt, which I still have and wear to this day.&lt;/p&gt;
&lt;h2&gt;Perlin Colours&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/perlin-colours.png&quot; alt=&quot;Perlin Colours&quot;&gt;&lt;/p&gt;
&lt;p&gt;Our final task was to create a project that heavily featured a variety of colours. One of our team had the idea to create a Perlin noise generator that could create plenty of colourful gradients. We decided to name it &lt;a href=&quot;https://github.com/Thesupernile/PerlinColours&quot;&gt;Perlin Colours&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After some research on the formulas involved and a fair bit of trial and error, we ended up with a functioning noise generator implemented with HTML Canvas. I had dabbled with Canvas before, but this was the first time I had implemented a proper renderer using it.&lt;/p&gt;
&lt;p&gt;We ended up sleeping on the problem, but once we woke up, we were fully refreshed and fixed the final bugs with the implementation. It was very satisfying to have a proper end product.&lt;/p&gt;
&lt;p&gt;The project was presented, and it went down a treat! The prizes were limited-edition laptop stickers that I have proudly stuck onto my laptop ever since.&lt;/p&gt;
&lt;h2&gt;Returning Home&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/hack-club-shipwrecked/boston-airport.jpg&quot; alt=&quot;Boston Logan International restaurant&quot;&gt;&lt;/p&gt;
&lt;p&gt;Alas, all good things must come to an end. It was the final day, so I gathered all my belongings and packed them away into a suitcase. My team attended the closing ceremony and said our goodbyes to the people there. There were so many nice people I met, and I&amp;#39;ll cherish the memories made forever.&lt;/p&gt;
&lt;p&gt;I caught an Uber alongside some others also needing a flight back and returned to the airport. It was there our team finally disbanded. One amusing thing at the airport was the Hack Club official plushies were just over the weight limit and were tripping all the security alarms. I got through just fine, but it caused a right mess!&lt;/p&gt;
&lt;p&gt;It was then I also got to see my very first American passport. Having only seen European ones, it was a right culture shock with pages full of landmarks and a giant eagle on the photo page.&lt;/p&gt;
&lt;p&gt;The actual flight was thankfully uneventful. I watched the in-flight entertainment and ate the food, but slowly drifted off to sleep until we landed.&lt;/p&gt;
&lt;h2&gt;Reflections&lt;/h2&gt;
&lt;p&gt;Participating in Shipwrecked is an experience I&amp;#39;ll remember forever, and I thank Hack Club and the organisers for helping set up such a wonderful event. I have still maintained contact with several of the other people there and arrange to meet up with them at future hackathons.&lt;/p&gt;
&lt;p&gt;It was also very enjoyable working in teams. Having spent so much of my time programming solo, it was nice bouncing ideas back and forth to come to informed conclusions.&lt;/p&gt;
&lt;p&gt;I&amp;#39;ve done many other things with Hack Club since, but they are perhaps a story for another blog post...&lt;/p&gt;
&lt;p&gt;One thing you can take for granted is that I&amp;#39;ll absolutely be doing more hackathons in the future!&lt;/p&gt;
</content:encoded><category>hack-club</category><category>hackathon</category></item><item><title>Building An Astro Blog</title><link>https://hawksley.dev/blog/building-astro-blog/</link><guid isPermaLink="true">https://hawksley.dev/blog/building-astro-blog/</guid><description>My developer experience trying out the Astro framework and ditching my old site.</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&amp;#39;ve owned the domain name hawksley.dev for a while now, but I&amp;#39;ve never done much with it aside from sending email. Over the weekend, I thought I might as well make good use of it and decided to create a blog.&lt;/p&gt;
&lt;p&gt;In the beginning, this site had a humble home page with some links to GitHub projects. A blog requires much more infrastructure for me to use it effectively. For one, it&amp;#39;d be great if I could just write my posts in Markdown and have them formatted by my project automatically. Having a look at the options available, the first that stood out was &lt;a href=&quot;https://jekyllrb.com/&quot;&gt;GitHub&amp;#39;s Jekyll.&lt;/a&gt; It looked nice and had great integration with GitHub Pages, which I&amp;#39;m hosting with at the time of writing. However, it just felt too rigid. I needed something modern that I felt I could get my hands dirty with.&lt;/p&gt;
&lt;p&gt;Enter Astro.&lt;/p&gt;
&lt;h2&gt;Why Astro&lt;/h2&gt;
&lt;p&gt;In the grand scheme of things, &lt;a href=&quot;https://astro.build/&quot;&gt;the Astro framework&lt;/a&gt; is pretty new at just 5 years old. That hasn&amp;#39;t prevented it from gaining popularity rapidly. It holds performance as a key design principle, anything that can be static will render statically. By default, it ships absolutely no JS to the browser, which felt perfect for my use case. I have no need for advertising or heavy tracking scripts weighing down my site. All I need is a place to write.&lt;/p&gt;
&lt;p&gt;Learning how to work with Astro was completely painless. I created a new GitHub repository and followed along with their very high-quality documentation to create a blog of my own. At the very end of it, I’d created a nice neat blog that loaded instantly and was easy to write for.&lt;/p&gt;
&lt;p&gt;I wasn&amp;#39;t satisfied by using the tutorial&amp;#39;s blog for my site, though, as it felt too cookie-cutter, and so I started again, now with confidence in the framework.&lt;/p&gt;
&lt;h2&gt;The Design Decisions&lt;/h2&gt;
&lt;p&gt;There were some definite design decisions I knew I wanted from the get-go. First-class light mode and dark mode support were a must. Plenty of blogs offered just one or the other, and after a bit of digging, it didn&amp;#39;t seem technically hard to implement at all.&lt;/p&gt;
&lt;p&gt;Another key principle was mobile-first design. Many of my previous projects started with the key assumption that the reader will be on a laptop or desktop, but that often isn&amp;#39;t the case. Mobile devices account for approximately two-thirds of global internet traffic, with the remaining third coming from desktops. I wanted this site to stand the test of time until I inevitably rebuild it again, so it&amp;#39;s only natural I designed it with mobile devices in mind.&lt;/p&gt;
&lt;p&gt;My final decision I knew I wanted was to have excellent performance scores, one of the very reasons that I chose Astro for the job. Components are almost a certainty in modern web design to ensure consistency across pages, but I needed to write components used at compile time, rather than at run time.&lt;/p&gt;
&lt;h2&gt;Actually Programming the Site&lt;/h2&gt;
&lt;p&gt;After creating an experimental fork of my main repository and wiping the directory, I initialised a new blank Astro project with &lt;code&gt;pnpm create astro@latest&lt;/code&gt; and scaffolded the directory structure I&amp;#39;d need.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;public // Assets such as robots.txt and favicons.
src
  assets // Any assets displayed in the actual site.
  components // Reusable components representing sections of UI.
  content // Markdown files representing the blog.
  layouts // The base layout each page uses.
  pages // The actual pages rendered, combining all parts together.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Development went very smoothly over two days and 15 full hours, with only some minor hiccups along the way.&lt;/p&gt;
&lt;p&gt;Google&amp;#39;s PageSpeed Insights flagged my planned accent colour International Orange &lt;code&gt;#FF4F00&lt;/code&gt; as having insufficient contrast in light mode, so I had to split into separate accents for light and dark mode. By using global CSS variables that change depending on the theme, I implemented this change painlessly.&lt;/p&gt;
&lt;p&gt;I also encountered some &lt;abbr title=&quot;Content Layout Shift&quot;&gt;CLS&lt;/abbr&gt; issues surrounding my choice of font. I personally love the look of the &lt;a href=&quot;https://rsms.me/inter/&quot;&gt;Inter font&lt;/a&gt; and included it in my project[^1]. However, although 300 KiB may not sound like a lot, on a slow connection it’s a noticeable delay. After some research I discovered the &lt;a href=&quot;https://github.com/fonttools/fonttools&quot;&gt;fontTools library&lt;/a&gt; and managed to shrink the Inter font to 70 KiB by subsetting the font to only the character sets I need.&lt;/p&gt;
&lt;h2&gt;The Conclusion&lt;/h2&gt;
&lt;p&gt;Astro has been a pleasure to work with, and I can easily see myself using it in future projects now I&amp;#39;ve adjusted to it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://hawksley.dev/blog/building-astro-blog/pagespeed-insights.webp&quot; alt=&quot;Perfect Google PageSpeed score&quot;&gt;&lt;/p&gt;
&lt;p&gt;A perfect score has got to be one of the most satisfying feelings after doing a site redesign.&lt;/p&gt;
&lt;p&gt;[^1]: I&amp;#39;m now using IBM Plex Sans and IBM Plex Mono, but they are subsetted to 62KiB and 9KiB respectively, so the advice still holds.&lt;/p&gt;
</content:encoded><category>astro</category><category>web</category></item></channel></rss>