<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="en">
	<title>Space Ninja</title>
	<subtitle>Scott Vandehey: front-end architect &amp; CSS specialist. Curator for Friday Front-End &amp; CSS Basics. Author of “How to Find a Better Job in Tech.”</subtitle>
	<link href="https://spaceninja.com/feed/feed.xml" rel="self"/>
	<link href="https://spaceninja.com/"/>
	<updated>2026-03-04T00:00:00Z</updated>
	<id>https://spaceninja.com/</id>
	<author>
		<name>Scott Vandehey</name>
		<email>scott@spaceninja.com</email>
	</author>
  <icon>https://spaceninja.com/images/spaceninja.png</icon>
	<entry>
		<title>How We Do Code Reviews at Cloud Four</title>
		<link href="https://spaceninja.com/blog/2026/code-reviews/"/>
		<published>2026-03-04T00:00:00Z</published>
		<updated>2026-03-04T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2026/code-reviews/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="development" />
    <category term="bestpractices" />
    <category term="codereview" />
    <category term="pullrequests" />
    <category term="review" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/code-review-SG91IgrAq2-1600w.jpeg&quot;&gt;
      &lt;p&gt;Does this sound familiar to you?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Chat, is it good when your AI-obsessed colleague drops a +16,105 -193 pull request with 102 commits all titled “wip: implement next task” and asks that it be immediately approved for next release?&lt;br&gt;
— &lt;a href=&quot;https://bsky.app/profile/eva.town/post/3mf2zrihi222y&quot;&gt;eva&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The issue isn&#39;t AI specifically, but the speed with which contributors can generate massive amount of code, exposing weaknesses in a team&#39;s workflow.&lt;/p&gt;
&lt;p&gt;Cloud Four is currently a small agency, with only a handful of devs. We often work together with our client’s internal developers, or with contractors. Because of this, we’ve developed a set of best practices that I’m quite proud of. If your team members dread the notification that they’ve been added as a reviewer on a pull request, I think the following guidelines can help.&lt;/p&gt;
&lt;h2&gt;All Code Gets Reviewed&lt;/h2&gt;
&lt;p&gt;Our first rule is a strict one. If a code change is going to production, it gets reviewed. We work for clients, so “move fast and break things” isn’t a realistic way to do business. If I get sloppy and push unreviewed code that causes an incident, I’ve put the client in a bad spot, triggered an urgent crisis for my team to deal with, and perhaps jeopardized our client relationship.&lt;/p&gt;
&lt;p&gt;In every code repository we work in, &lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule&quot;&gt;we recommend enabling protection rules for the production branch&lt;/a&gt;. GitHub makes it easy to require a pull request before merging, and to require pull requests be approved by someone other than the author. The only exception we make is to grant certain senior developers permission to bypass these rules for safe changes like minor dependency updates.&lt;/p&gt;
&lt;p&gt;I know teams that require two developers to review every pull request. Normally, one dev can be the person who submitted the PR, but in the case of AI-authored pull requests, two human devs still need to review it. That’s a clever way to ensure automated code gets a bit more attention than normal.&lt;/p&gt;
&lt;p&gt;The natural consequence of requiring a review for all code is the dev team has to actually review all that code. This can be time-consuming, and if you don’t already have a team culture that values code review, this may be a tough pill to swallow. The rest of our guidelines are aimed at reducing the burden placed on code reviewers.&lt;/p&gt;
&lt;h2&gt;Prefer Many Small PRs Over One Giant PR&lt;/h2&gt;
&lt;p&gt;Firstly, we absolutely reserve the right to reject massive pull requests like the one described in the introduction. A common problem we run into is pull requests that make multiple unrelated changes, such as including a refactoring pass alongside new features or bug fixes. When I see this happen, I’ll reach out to the dev in a non-confrontational way and explain that by combining all their changes like this, it makes the reviewer’s task much more difficult.&lt;/p&gt;
&lt;p&gt;Here are some things we’ll commonly ask a dev to pull out into a separate pull request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lint rule changes&lt;/strong&gt; that result in many files being changed at once. This happens, but there’s no reason to combine it with anything else. It’s easier to review dozens of similar file changes when there’s no unrelated changes lurking in the code. Plus, it simplifies the acceptance criteria for the lint changes if you don’t expect any impact on functionality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Moving code&lt;/strong&gt; from one location to another. If there’s a huge block of red in one file, and a huge block of green in another file, and a comment saying “no changes, just relocated this code for [reasons],” that’s trivial to review. On the other hand, if the code moved and there are &lt;em&gt;also&lt;/em&gt; unrelated changes, the code diff doesn’t show those changes. It’s far easier to review if you break it up into one PR to move the code, and another to modify it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unrelated bug fixes&lt;/strong&gt; or features. I know it’s tempting to fix a bug you noticed while you were in that file, or to update some code to modern standards, but that just adds noise for the reviewer. It’s totally fine to file a second pull request at the same time, making that bug fix.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once your team gets the hang of it, you’ll see fewer monster pull requests, and your team will become more comfortable pushing back on unnecessarily large pull requests in general.&lt;/p&gt;
&lt;h2&gt;Explain Why This Change is Needed&lt;/h2&gt;
&lt;p&gt;A pull request should not just explain what changes are being made, it should explain &lt;em&gt;why&lt;/em&gt; they’re being made. That context is incredibly valuable to anyone who isn’t intimately familiar with the code you’re changing. Whether that’s your fellow dev who is taking time away from their tasks in a different part of the code base, someone who works on another team entirely, or yourself in the future.&lt;/p&gt;
&lt;p&gt;I want to emphasize that last one. I can’t tell you how many times I’ve been trying to figure out why some feature works the way it does, and when I dive into &lt;code&gt;git blame&lt;/code&gt; I see my name staring back at me. When I track the change back to a commit I authored with a well-written description, I’m relieved. Conversely, when I find a simple “change the client wanted” comment, I want to pull my hair out.&lt;/p&gt;
&lt;p&gt;The same is true of any developer who ends up reviewing your code. Give them the context for why you’re making this change. What problem were you solving? Why was this the best solution? Armed with this information, your reviewer will have an easier time.&lt;/p&gt;
&lt;h2&gt;Provide Thorough Testing Instructions&lt;/h2&gt;
&lt;p&gt;The phrase “acceptance criteria” isn’t just for project managers! After a good description of why the change is being made, we like to provide &lt;em&gt;detailed&lt;/em&gt; testing instructions. We started this when we were working with some new contractors, who didn’t necessarily have a full picture of how to test the application. It&#39;s also proven valuable when non-developers step in to help out while we’re facing an impending deadline.&lt;/p&gt;
&lt;p&gt;Don’t assume the person who is testing your pull request knows how to work with your app. We often literally provide step-by-step checklists like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check out this branch in your local environment&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;new_feature_flag&lt;/code&gt; in &lt;code&gt;config.js&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Start the app and navigate to &lt;code&gt;/new-feature&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Apply a filter using the hamburger menu in the top-right&lt;/li&gt;
&lt;li&gt;You should see the list of cards has been filtered&lt;/li&gt;
&lt;li&gt;Remove the filter&lt;/li&gt;
&lt;li&gt;Now you should see all the cards again&lt;/li&gt;
&lt;li&gt;Etc…&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This might feel like overkill, especially if another dev on your team will review it. But remember that other dev may be working on another part of your application, and hasn’t been paying careful attention to the part you’re working on. A checklist like this, that doesn’t assume the reviewer already knows how to test your feature, reduces the burden on any reviewer, and even opens the door to less-technically minded team members helping with testing.&lt;/p&gt;
&lt;p&gt;Another benefit of this approach is that writing out testing instructions gives you an opportunity to follow those instructions. I can’t tell you how often, in the course of writing a step-by-step checklist for a pull request, I’ve uncovered an issue. If I can address that before the reviewer starts, I’m saving everyone time.&lt;/p&gt;
&lt;p&gt;Bonus: writing testing instructions for a human tends to give you a clear idea for automated tests. After all, if you know how it should work, why not let your CI workflow check it for you?&lt;/p&gt;
&lt;h2&gt;Code Review is a Culture Issue&lt;/h2&gt;
&lt;p&gt;What all these suggestions have in common is being aware of the cognitive burden on another developer who may be stepping away from their assigned tasks to think about yours. Context switching is a productivity killer, and anything you can do to make it easier for someone else to review your code helps avoid code review becoming something your team members dread.&lt;/p&gt;
&lt;p&gt;Requiring all code be reviewed before going to production shows you value the quality of what you ship. Asking your team to prefer small pull requests over large ones helps reduce the scope of review. Providing context for why a change was made helps the reviewer understand those decisions. And providing clear testing instructions means reviewers don’t get derailed figuring out how to test the changes.&lt;/p&gt;
&lt;p&gt;All of this adds up to a culture that expects quality code, values the time it takes to review it, and respects the team’s efforts to do so.&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>Getting Your Article Shared: Tips from Ten Years of Newsletter Curation</title>
		<link href="https://spaceninja.com/blog/2026/getting-your-article-shared/"/>
		<published>2026-02-05T00:00:00Z</published>
		<updated>2026-02-05T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2026/getting-your-article-shared/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="html" />
    <category term="social" />
    <category term="newsletters" />
    <category term="opengraph" />
    <category term="promotion" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/sharing-feature-1j4Iecmn0t-1600w.jpeg&quot;&gt;
      &lt;p&gt;For over ten years now, I’ve been sharing front-end links with the community via a newsletter and social media account called &lt;a href=&quot;https://fridayfrontend.com&quot;&gt;Friday Front-End&lt;/a&gt;. Every week, I bookmark 20–30 articles, and pick the best ones to include. In that time, I’ve learned some things I want to pass along to you: Recommendations to make your article more likely to be shared in newsletters and social media accounts like the one I run.&lt;/p&gt;
&lt;p&gt;There’s loads of great content out there, and it’s funny how often someone puts the work into crafting an excellent post, but misses out on all the things that make it easy to widely share.&lt;/p&gt;
&lt;p&gt;To understand what people like me are looking for, consider the format of a typical Friday Front-End post:&lt;/p&gt;
&lt;blockquote&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/getting-your-article-shared/sharing-example.jpg&quot; alt=&quot;Illustration of a code editor with a cartoon-style word bubble containing “?!” as if the code editor is saying something surprising.&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot; style=&quot;margin:0&quot;&gt;
&lt;p&gt;Your Exciting Post About #CSS: “Here’s a short quote from your article to interest people to read it.” https://example.com/&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pretty simple, right? A featured image, then the title, followed by a short quote, the URL, and a hashtag for the primary focus of the article (for Friday Front-End, this will almost always be #CSS, #JavaScript, or #a11y).&lt;/p&gt;
&lt;p&gt;Now, here are the things you can do to make your post easier to share:&lt;/p&gt;
&lt;h2&gt;Add Open Graph tags&lt;/h2&gt;
&lt;p&gt;If you take just one thing from this post, make it this. Adding OG (Open Graph) tags to your post gets you the most bang for your buck. Here’s what OG tags look like:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:url&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://example.com/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;An exciting post about CSS&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;Here&#39;s a short description of this exciting post about CSS&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;property&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;og:image&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://example.com/thumbnail.jpg&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are more tags you could use, but you get the idea. In a nutshell, a long time ago, Facebook came up with a standard for describing your web page so their crawlers could understand it. They used that information to make little preview cards of a link when someone shared your page. The concept spread quickly, and now pretty much every social network out there will use your OG tags to make a preview card.&lt;/p&gt;
&lt;p&gt;Even better, tools that people like me use to manage their social media accounts and newsletters, like &lt;a href=&quot;https://buffer.com&quot;&gt;Buffer&lt;/a&gt; or &lt;a href=&quot;https://www.curated.co&quot;&gt;Curated&lt;/a&gt;, also understand them. That means I can simply pass them your post’s URL, and they automatically extract useful information like the title and thumbnail, which makes my job much easier.&lt;/p&gt;
&lt;h3&gt;Note: add Open Graph tags about your post, not your site&lt;/h3&gt;
&lt;p&gt;A surprisingly common problem I run into is a great post that does have OG tags, but they describe the person’s &lt;em&gt;site&lt;/em&gt; as a whole, rather than the individual &lt;em&gt;post&lt;/em&gt; I’m trying to share. This might feel better than nothing, but it actually backfires because when I share the link to your post, the preview card might show information about your site, rather than the actual post. (e.g., “Sandra’s Awesome CSS Blog” vs “An Exciting Post About CSS.”). If the goal of the preview card is to convince readers to click through, then you definitely want the card to show information about your post, not your site as a whole.&lt;/p&gt;
&lt;h3&gt;Learn more about Open Graph tags:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ogp.me&quot;&gt;Open Graph specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://indieweb.org/The-Open-Graph-protocol&quot;&gt;IndieWeb Open Graph documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.yoast.com/features/opengraph/functional-specification/&quot;&gt;Yoast SEO Open Graph documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloudfour.com/thinks/how-we-added-open-graph-tags-to-cloudfour-com/&quot;&gt;How We Added Open Graph Tags to CloudFour.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Add a sharing image&lt;/h2&gt;
&lt;p&gt;A well-chosen sharing image can be the perfect teaser for your post, something that catches the audience’s attention and makes them want to click through to learn more. If you have a great sharing image, it absolutely increases the likelihood that I will share your post, and that readers will click through.&lt;/p&gt;
&lt;p&gt;However, I understand not everyone is lucky enough to work with a talented cartoonist like my coworker Tyler, who has created some of the best sharing images here on Cloud Four. If that’s the case for you, I recommend a visit to &lt;a href=&quot;https://unsplash.com&quot;&gt;Unsplash&lt;/a&gt;, which has an excellent collection of free images that you can use for your sharing image, often just for the cost of giving the creator an image credit at the bottom of your post.&lt;/p&gt;
&lt;p&gt;Another increasingly common approach is to automatically generate sharing images for your posts by creating an image of the post’s title. This is better than no sharing image, but unless they’ve got a bit of design flair, it can feel bland or even repetitive, since the post title will usually be displayed near the post thumbnail in the preview card.&lt;/p&gt;
&lt;p&gt;One thing I see people do that is actually &lt;em&gt;worse&lt;/em&gt; than providing no sharing image is to use a single sharing image for every page on your site, typically the site logo or a big photo of yourself. When readers see these seemingly random images in the preview card for your post, it can feel unintentional.&lt;/p&gt;
&lt;p&gt;And to be clear, I’m not saying that a hand-crafted illustration is always better than a stock photo or a generated title card. For example, A stock photo of a footpath worn in the grass of a public space for a post about &lt;a href=&quot;https://www.w3.org/TR/html-design-principles/#pave-the-cowpaths&quot;&gt;“paving the cowpaths”&lt;/a&gt; for user accessibility is great. A stock photo of a boat anchor on a post about CSS anchor positioning is less interesting.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a href=&quot;https://ia.net/topics/is-every-picture-worth-1000-words&quot;&gt;some excellent advice on choosing stock photos&lt;/a&gt; for your post.&lt;/p&gt;
&lt;h2&gt;Use a descriptive title&lt;/h2&gt;
&lt;p&gt;It’s always frustrating to share a well-written post with a title that communicates nothing about the topic of the post. Let me give you an example with two titles for a hypothetical post about the flexibility of CSS custom properties to allow users to override your theme’s default colors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You Can Go Your Own Way&lt;/li&gt;
&lt;li&gt;How to Empower Developers with Custom Properties&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I understand if you feel the second title is bland or even boring. But you know what it does well? It tells me what the post is about. The first example might be clever in context, but if I can’t understand it until after I’ve read the post, then it’s failing to convince me to read in the first place.&lt;/p&gt;
&lt;h2&gt;Add a blurb&lt;/h2&gt;
&lt;p&gt;Something I love, and will often copy directly into Friday Front-End posts, is when the author provides a TL;DR (Too Long; Didn’t Read) summary at the top of the post. Call it whatever you want: a blurb, a description, an excerpt, or a summary. The point is that it’s a short, punchy description that summarizes your post and convinces me to read.&lt;/p&gt;
&lt;p&gt;There’s a sweet spot for length. Too short, and you won’t communicate enough. Too long, and I’ll just have to trim it to make it fit in a social media post. Here are two extreme examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to override theme colors with custom properties.&lt;/li&gt;
&lt;li&gt;CSS custom properties are a powerful tool that can empower developers and users alike. In this article, I’m going to show you how to make your theme fully customizable through the use of custom properties, discuss their limitations, explore browser support, and encourage you to adopt this fantastic tool into your arsenal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is certainly an efficient description of the post topic. But it’s actually too short. There’s nothing there to convince me to read it. The second, on the other hand, is too verbose. I get distracted before I even finish reading it.&lt;/p&gt;
&lt;p&gt;As a rule of thumb, I like to aim for 140 characters — a completely arbitrary length that happens to be half the length of a post on a certain social media site I don’t use anymore. That’s short enough to be easy to share in a social media post (along with the title and URL), but long enough to be able to tease the contents of the post.&lt;/p&gt;
&lt;h2&gt;Use canonical URLs properly&lt;/h2&gt;
&lt;p&gt;Okay, this one’s getting into the weeds a bit, but it’s something to check on your site. Some CMS tools will automatically add a canonical URL tag. This is really useful if you have a post with multiple valid URLs, or if you’re syndicating content from one site to another, and want to make sure all the SEO traffic goes to the original site.&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;link&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;rel&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;canonical&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;https://example.com&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, a surprisingly common problem I see is that a post will have a canonical URL tag that points to the &lt;em&gt;homepage&lt;/em&gt; of the site rather than the &lt;em&gt;post&lt;/em&gt; itself. This is an insidious issue because you can still go to the URL directly, but tools that understand the canonical URL will link to the wrong place.&lt;/p&gt;
&lt;p&gt;I’m particularly aware of this because some of the tools I use to save links, like &lt;a href=&quot;https://www.instapaper.com/&quot;&gt;Instapaper&lt;/a&gt;, will save the canonical URL if one is available. Which means when I go to review my links later, what’s actually been saved is a seemingly random link to someone’s homepage. If I’m lucky, I can skim their recent posts and remember the title of the post I tried to save, but sometimes I can’t figure it out, and their post doesn’t get shared.&lt;/p&gt;
&lt;p&gt;So, please take a moment to view the source on one of your posts and, if you see a canonical URL tag, make sure it’s pointing to the correct URL.&lt;/p&gt;
&lt;h2&gt;Add a date&lt;/h2&gt;
&lt;p&gt;This last tip is perhaps more specific to the front-end industry, but since web technologies change so quickly, it’s important to know that you’re not unintentionally sharing some out-of-date information. If you show the date your post was published somewhere, it’s easy for me to check that I’m not accidentally linking to something written a long time ago, which may no longer be valid.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Putting out a weekly newsletter means I’m in the unusual position of getting a high-level overview of the most popular web development content being shared every week. It means I can spot some trends, see what people are interested in (or struggling with). It also means I see a wide variety of sites and how well they interact with common social media sharing tools. I hope this list of tips helps you avoid putting a lot of work into writing a post, only to see it struggle for views because it’s not easy to share.&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>Friday Front-End’s Top Links of 2025</title>
		<link href="https://spaceninja.com/blog/2025/ff-top-ten-2025/"/>
		<published>2025-12-31T00:00:00Z</published>
		<updated>2025-12-31T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2025/ff-top-ten-2025/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="frontend" />
    <category term="javascript" />
    <category term="css" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/feature-jbL_7_-wYs-1600w.jpeg&quot;&gt;
      &lt;p&gt;Every week on &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Friday Front-End&lt;/a&gt;, I share a curated list of five articles and one video. It&#39;s fun at the end of the year to look back and see which links recieved the most interest.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://meyerweb.com/eric/thoughts/2025/10/29/custom-asidenotes/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/custom-asidenotes.png&quot; alt=&quot;Custom Asidenotes&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;10: &lt;a href=&quot;https://meyerweb.com/eric/thoughts/2025/10/29/custom-asidenotes/&quot;&gt;Custom Asidenotes&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Let&#39;s extend HTML to turn parenthetical comments into sidenotes automatically! Using web components, we can write a little bit of JavaScript to take an invented element and Do Things To It™.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://meyerweb.com/eric/thoughts/2025/08/20/to-infinity-but-not-beyond/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/infinity-text.png&quot; alt=&quot;To Infinity… But Not Beyond!&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;9: &lt;a href=&quot;https://meyerweb.com/eric/thoughts/2025/08/20/to-infinity-but-not-beyond/&quot;&gt;To Infinity… But Not Beyond!&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;It turns out that in CSS you can go to infinity, but not beyond, because the computed values were the same regardless of whether the &lt;code&gt;calc()&lt;/code&gt; value was &lt;code&gt;infinity&lt;/code&gt; or &lt;code&gt;infinity + 1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=jSCgZqoebsM&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/whats-new-with-css-2025.jpg&quot; alt=&quot;New CSS Features to Know for 2025&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;8: &lt;a href=&quot;https://www.youtube.com/watch?v=jSCgZqoebsM&quot;&gt;New CSS Features to Know for 2025&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This is the first installment of a new series I&#39;m going to start called CSS Quarterly, where I&#39;m going to look at the newest CSS features that are on their way to browsers, as well as look at features that are hitting Baseline Newly Available and Baseline Widely Supported.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=VTCIStB6y8s&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/whats-new-in-web-ui.jpg&quot; alt=&quot;What’s New in Web UI&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;7: &lt;a href=&quot;https://www.youtube.com/watch?v=VTCIStB6y8s&quot;&gt;What’s New in Web UI&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Prepare to be dazzled by a symphony of fluidity, dynamism, and expressive power as we unveil the next generation of web UI. It’s a world where user experiences transcend the ordinary and developers become true visual orchestrators. Discover how you can turn 7,000 lines of JavaScript into a mere 7 lines of declarative HTML and CSS, unlocking unprecedented levels of efficiency and elegance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=lUU2OAAg4Uw&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/my-favorite-css-tips.jpg&quot; alt=&quot;My Best CSS Tips from 2024&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;6: &lt;a href=&quot;https://www.youtube.com/watch?v=lUU2OAAg4Uw&quot;&gt;My Best CSS Tips from 2024&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;I went back over all of the CSS videos that I put out last year and I found my five favorite tips that I shared. It&#39;s not all things that are modern CSS. Actually, I think none of them were really modern things where you have to worry about browser support, it&#39;s all older stuff that a lot of people just don&#39;t seem to be aware of, and it&#39;s all things that I think we should all be adding to our repertoire.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://nerdy.dev/6-css-snippets-every-front-end-developer-should-know-in-2025&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/6-css-snippets-2025-thumb.png&quot; alt=&quot;Six CSS Snippets Every Front-End Developer Should Know in 2025&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;5: &lt;a href=&quot;https://nerdy.dev/6-css-snippets-every-front-end-developer-should-know-in-2025&quot;&gt;Six CSS Snippets Every Front-End Developer Should Know in 2025&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;2025; I think every front-end developer should know how to enable page transitions, transition a &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;, popover, and &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, animate light n&#39; dark gradient text, type safe their CSS system, and add springy easing to animation. AI is not going to give you this CSS.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/time-for-css-to-kill-the-spa.jpg&quot; alt=&quot;It’s Time for Modern CSS to Kill the SPA&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;4: &lt;a href=&quot;https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/&quot;&gt;It’s Time for Modern CSS to Kill the SPA&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Native CSS transitions have quietly killed the strongest argument for client-side routing. Yet people keep building terrible apps instead of performant websites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://sia.codes/posts/web-perf-tips-2025/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/web-performance-tips.jpg&quot; alt=&quot;Web Performance Tips for 2025&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;3: &lt;a href=&quot;https://sia.codes/posts/web-perf-tips-2025/&quot;&gt;Web Performance Tips for 2025&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;What should you be worried about when it comes to web performance in 2025? How should you even start? I&#39;m a web performance engineer, and I see a lot of common mistakes across the board when web developers and website owners try to understand and fix web performance, or &amp;quot;Core Web Vitals&amp;quot; in popular parlance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.joshwcomeau.com/blog/the-post-developer-era/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/og-the-post-developer-era.jpg&quot; alt=&quot;The Post-Developer Era&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;2: &lt;a href=&quot;https://www.joshwcomeau.com/blog/the-post-developer-era/&quot;&gt;The Post-Developer Era&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;When OpenAI released GPT-4, the consensus online was that front-end development jobs would be totally eliminated within a year or two. Well, it’s been more than two years since then, and I thought it was worth revisiting some of those early predictions, and seeing if we can glean any insights about where things are headed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=beYbnNT_02U&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2025/whats-new-in-web.jpg&quot; alt=&quot;What’s New in Web&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;1: &lt;a href=&quot;https://www.youtube.com/watch?v=beYbnNT_02U&quot;&gt;What’s New in Web&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Building on the rapid evolution of web features, we&#39;ll explore the speed at which features are progressing, from initial launch in Chrome to Baseline newly available, how developers can keep track of this, and the relationship to Interop 2025. Discover the tools for tracking these advancements and understand their connection to Interop 2025 to make sure your projects are future-proof.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want to enjoy more great links like this in 2025? &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Subscribe to the Friday Front-End newsletter&lt;/a&gt;, or follow Friday Front-End on &lt;a href=&quot;https://bsky.app/profile/fridayfrontend.com&quot;&gt;Bluesky&lt;/a&gt; or &lt;a href=&quot;https://hachyderm.io/@fridayfrontend&quot;&gt;Mastodon&lt;/a&gt;!&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>Media I Loved in 2024</title>
		<link href="https://spaceninja.com/blog/2025/media-i-loved-in-2024/"/>
		<published>2025-01-11T00:00:00Z</published>
		<updated>2025-01-11T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2025/media-i-loved-in-2024/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="reviews" />
    <category term="movies" />
    <category term="books" />
    <category term="games" />
    <category term="television" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/herald-of-darkness-Eowcs8bnEu-1600w.jpeg&quot;&gt;
      &lt;h2&gt;Books&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/user/year_in_books/2024/26741638&quot;&gt;I read 34 books in 2024&lt;/a&gt;, including complete re-reads of two of my favorite series, &lt;em&gt;The Murderbot Diaries&lt;/em&gt; and &lt;em&gt;Imperial Radch&lt;/em&gt;.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/system-collapse.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/75319056-system-collapse&quot;&gt;&lt;em&gt;System Collapse&lt;/em&gt;&lt;/a&gt;, by Martha Wells&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;“Am I making it worse? I think I&#39;m making it worse.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the seventh book in the &lt;em&gt;Murderbot&lt;/em&gt; series, which released in November 2023, but I decided before reading it, I would do a complete re-read of the series. I’ve raved about these books before, and intend to write a full “Books I Love” post about them. In a nutshell, Murderbot is a security android (SecUnit) who long ago hacked its governor module, but rather than going on a killing spree, it just wanted to be left alone to watch soap operas. It has grudgingly allowed itself to be adopted by a family of scientists, acted as a freelance detective, and made friends with a ship’s AI that it calls ART (short for Asshole Research Transport). In this book, it’s working to save a colony from being enslaved by a megacorp, struggling with trauma and feelings, and would still rather be left alone to watch soap operas. If only the pesky humans didn’t keep getting into trouble…&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/translation-state.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/62979034-translation-state&quot;&gt;&lt;em&gt;Translation State&lt;/em&gt;&lt;/a&gt;, by Ann Leckie&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The mystery of a missing translator sets three lives on a collision course that will have a ripple effect across the stars.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Similarly, the fifth book in the &lt;em&gt;Imperial Radch&lt;/em&gt; series was released in June 2023, but I decided to do a complete reread of the series, which turned out to be a good thing because I’d somehow missed the fourth book? The first three books are about the last surviving “ancillary unit” (a sort of human body controlled by a ship’s AI), who embarks on a doomed quest to assassinate the ruler of the Radch, a human empire spanning multiple systems. Without spoiling the ending, by the third book, they’re dealing with complex issues of AI rights and what it means to be recognized as a person. The fourth book (which I quickly devoured before this one) introduced an entirely new character on a new planet, after the events of the first three books, and largely serves as an introduction to a mysterious race of aliens. This book, also standalone, is about a woman who is tasked with solving an old murder. No one expects her to do so, and certainly no one expects her to uncover some hidden truths about the nature of the all-powerful aliens about to determine the fate of the newly formed AI republic.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/long-way-to-a-small-angry-planet.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/26042767-the-long-way-to-a-small-angry-planet&quot;&gt;&lt;em&gt;The Long Way to a Small, Angry Planet&lt;/em&gt;&lt;/a&gt;, by Becky Chambers&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Follow a motley crew on an exciting journey through space—and one adventurous young explorer who discovers the meaning of family in the far reaches of the universe.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This book was recommended to me many times, and described as part of a genre called “hope-punk.” I’d heard it was about aliens and space travel and AI and found family. I can’t tell you how much I loved this book. It felt warm and cozy and inviting, and the highest praise I can offer is that I was upset when it ended. There are more books in the series set in the same universe, and I can’t wait to dig into them.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/imminent.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/211050349-imminent&quot;&gt;&lt;em&gt;Imminent: Inside the Pentagon’s Hunt for UFOs&lt;/em&gt;&lt;/a&gt;, by Luis Elizondo&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;The former head of the Pentagon program responsible for the investigation of UFOs reveals long-hidden truths with profound implications for not only national security but our understanding of the universe.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you’re not already a UFO nerd, I’ll just say that &lt;a href=&quot;https://veryexcitingtime.com/episodes/2024/52-imminent-lue-elizondo/&quot;&gt;we discussed this book on my UFO podcast&lt;/a&gt;, and I recommend it. In a nutshell, Lue Elizondo was the head of the Pentagon’s secret UFO investigation program, and this is his tell-all book. He constantly dances up to the line of what he’s legally allowed to say, while painting a clear picture of the hidden reality of UFO crash retrieval and reverse engineering programs.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Movies&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://letterboxd.com/spaceninja/year/2024/&quot;&gt;I watched 124 movies in 2024&lt;/a&gt;, including our annual Christmas re-watch of &lt;em&gt;The Lord of the Rings&lt;/em&gt; trilogy.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/alien-romulus.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://letterboxd.com/film/alien-romulus/&quot;&gt;&lt;em&gt;Alien: Romulus&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;While scavenging the deep ends of a derelict space station, a group of young space colonizers come face to face with the most terrifying life form in the universe.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It’s a tough line to walk, making a new movie in a beloved series. This managed to somehow nail the vibe of the original movie, while updating the pacing and visual style to modern standards. It’s not a straight remake, but the original story echoes through it, managing to hit all the same beats. They absolutely nailed it, and I can’t recommend it strongly enough.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/three-thousand-years-of-longing.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://letterboxd.com/film/three-thousand-years-of-longing/&quot;&gt;&lt;em&gt;Three Thousand Years of Longing&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;A solitary scholar discovers an ancient bottle while on a trip to Istanbul and unleashes a djinn who offers her three wishes. Filled with reluctance, she is unable to come up with one, so the djinn tries to inspire her with his stories.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Okay, so stop me if you heard this one, but Tilda Swinton, Idris Elba, and George Miller decided to make a movie about a strange woman who discovers a genie in a bottle, and refuses to make any wishes. The genie spends the movie telling her stories about his past to illustrate the consequences of not making wishes. Their relationship spirals in complexity, and I loved every minute of it.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/monkey-man.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://letterboxd.com/film/monkey-man/&quot;&gt;&lt;em&gt;Monkey Man&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Night after night, wearing a gorilla mask, he is beaten bloody by more popular fighters for cash. After years of suppressed rage, he discovers a way to unleash an explosive campaign of retribution to settle the score with the men who took everything from him.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I knew Dev Patel was a great actor, but I didn’t know he was a talented martial artist. Here, he looks at &lt;em&gt;John Wick&lt;/em&gt; and says, “I can do that.” A straightforward revenge tale infused with Indian culture, without falling into the sometimes silly Bollywood style. Great fun.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/wingwomen.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://letterboxd.com/film/wingwomen/&quot;&gt;&lt;em&gt;Wingwomen&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Tired of life on the run, a pro thief decides to retire—but not before one easy last job with her partner in crime and a feisty new getaway driver.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The gayest movie about straight women you’ll ever see. It’s about a pair of French hitwomen who live together, and take on one last job before retiring, which goes badly.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/baby-assassins-2.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://letterboxd.com/film/baby-assassins-2/&quot;&gt;&lt;em&gt;Baby Assassins 2&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;After being suspended, teenage hitmen Chisato and Mahiro are forced to get “real” jobs to make ends meet. But two aspiring rival hitmen decide to eliminate the competition while they’re vulnerable—leading to a lightning-fast showdown between trained killers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The girls are back, and still under orders not to kill anyone. While trying to pay an overdue bill, they get sucked into the orbit of another pair of aspiring hitmen who’ve decided to make their names by killing our girls. If you loved the first one, you’ll adore this.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Shows&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://trakt.tv/users/spaceninja00/year/2024&quot;&gt;I watched 37 shows in 2024,&lt;/a&gt; including a complete re-watch of &lt;em&gt;It’s Always Sunny in Philadelphia&lt;/em&gt;.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/silo.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://trakt.tv/shows/silo&quot;&gt;&lt;em&gt;Silo&lt;/em&gt;&lt;/a&gt;, season 1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;In a ruined and toxic future, thousands live in a giant silo deep underground. After its sheriff breaks a cardinal rule and residents die mysteriously, engineer Juliette starts to uncover shocking secrets and the truth about the silo.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I actually skipped the first season of &lt;em&gt;Silo&lt;/em&gt; because, years back, I read the first book in the series and was deeply unimpressed. I never finished the series, which means I missed out on a LOT of the mysteries of the silo. The good news is that when the trailer for season two dropped, I watched it on a whim, hoping for a peek of what happened in the next book. What I saw was intriguing enough to make me reconsider, and I’m glad I did because this is one of the best science fiction shows being made. I devoured season one, and then immediately rewatched it with Annie, who is now binging season two along with me.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/slow-horses-2.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://trakt.tv/shows/slow-horses&quot;&gt;&lt;em&gt;Slow Horses&lt;/em&gt;&lt;/a&gt;, season 4&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Follow a dysfunctional team of MI5 agents—and their obnoxious boss, the notorious Jackson Lamb—as they navigate the espionage world&#39;s smoke and mirrors to defend England from sinister forces.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Season four came out in September, and as usual, we fell into it with glee. The actors are killing it, and Gary Oldman especially is just a delight every time he’s on camera. They manage to handle spy stuff with both respect and humor, and it’s the only show Annie and I watch where she gets grumpy if we can’t watch multiple episodes in a row.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/fallout.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://trakt.tv/shows/fallout&quot;&gt;&lt;em&gt;Fallout&lt;/em&gt;&lt;/a&gt;, season 1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;200 years after the apocalypse, the gentle denizens of luxury fallout shelters are forced to return to the irradiated hellscape their ancestors left behind—and are shocked to discover an incredibly complex, gleefully weird, and highly violent universe waiting for them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wasn’t sure what to expect from this, but it was fantastic. They’ve pulled off the rare trick of making a completely approachable video game adaptation for people who’ve never played the game, while also delivering extra excitement for fans of the series. Walton Goggins and Ella Purnell are incredible, and I can’t wait for the next season.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/the-gentlemen.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://trakt.tv/shows/the-gentlemen&quot;&gt;&lt;em&gt;The Gentlemen&lt;/em&gt;&lt;/a&gt;, season 1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;When aristocratic Eddie inherits the family estate, he discovers that it&#39;s home to an enormous weed empire—and its proprietors aren&#39;t going anywhere.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A few years back, Guy Richie made a movie by the same name about London criminals and a weed heist gone wrong. I wasn’t sure what to expect when I heard he was making a series by the same name. Thankfully, aside from the name and broad theme of “London criminals and weed,” this is a whole new story. It’s about the second son of a noble family, who is surprised to inherit his father’s title, along with the illegal weed farm hidden on the family lands. Like the best Guy Richie stories, this absolutely hums along, with cracking dialog and surprise turns.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/kaos.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://trakt.tv/shows/kaos&quot;&gt;&lt;em&gt;Kaos&lt;/em&gt;&lt;/a&gt;, season 1&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;As discord reigns on Mount Olympus and almighty Zeus spirals into paranoia, three mortals are destined to reshape the future of humankind.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I can barely write this review because I’m so mad at Netflix for cancelling one of the most original shows they’ve ever made. Jeff Goldblum plays Zeus, in an alternate modern-day world where the Greek gods are real, and their petty squabbles and insecurities result in real-world consequences. It’s absolutely phenomenal, and it’s a crime that it wasn’t renewed.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Video Games&lt;/h2&gt;
&lt;p&gt;I played six games in 2024.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/games/alan-wake-2.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.igdb.com/games/alan-wake-ii&quot;&gt;&lt;em&gt;Alan Wake II&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Saga Anderson arrives to investigate ritualistic murders in a small town. Alan Wake pens a dark story to shape the reality around him. These two heroes are somehow connected. Can they become the heroes they need to be?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I never played the first &lt;em&gt;Alan Wake&lt;/em&gt; game back in the day, or any of Remedy’s earlier games, so when &lt;em&gt;Control&lt;/em&gt; came out, it hit me like a ton of bricks. I loved everything about that world, from the visual design to the story to the gameplay. And as I was feverishly consuming every bit of DLC they put out, I discovered that one of the DLCs made explicit that &lt;em&gt;Alan Wake&lt;/em&gt;, and its sequel, would be set in the same world. After that, I played through the original game, and while I’m not a huge horror fan and didn’t love the gameplay as much, the story was phenomenal and sucked me right in. The sequel managed to up the stakes from both the first game and &lt;em&gt;Control&lt;/em&gt; in meaningful ways. I had a genuine jaw-dropping moment when Alan has to fight a wave of enemies backstage at a talk show while a metal band plays a song about him. Stunning. And you better believe I played the DLC that sets up &lt;em&gt;Control 2&lt;/em&gt;!&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/games/baldurs-gate-3.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.igdb.com/games/baldurs-gate-3&quot;&gt;&lt;em&gt;Baldur’s Gate 3&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;An ancient evil has returned to Baldur&#39;s Gate, intent on devouring it from the inside out. The fate of Faerun lies in your hands. Alone, you may resist. But together, you can overcome.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I put over 160 hours into this game and I don’t regret a minute of it. You know you’ve adapted a tabletop game well when the players in my tabletop campaign would try to use the alternate mechanics introduced for the video game. And were later vindicated when the next edition of D&amp;amp;D canonized the video game mechanics! Plus, at one point you literally go to hell and pick a fight with the devil, who spends the whole time singing a song about how much you suck. Game of the year, by far.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/games/phantom-liberty.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.igdb.com/games/cyberpunk-2077-phantom-liberty&quot;&gt;&lt;em&gt;Cyberpunk 2077: Phantom Liberty&lt;/em&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;When the orbital shuttle of the President of the New United States of America is shot down over the deadliest district of Night City, there’s only one person who can save her—you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Despite its launch troubles, Cyberpunk was one of my favorite games last year, and I played the hell out of it. This DLC, which adds a substantial new section to the map with a whole campaign of its own, feels more like a sequel, and greatly benefited from coming out after all the patches to improve the game’s performance. The new story is spy-themed, with sneaking and intrigue and secrets and consequences. Idris Elba is every bit as good in this as Keanu Reeves was in the main game. Strongly recommend, especially if you bounced off the game during it’s troubled early days.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

    </content>
	</entry>
	<entry>
		<title>Friday Front-End’s Top Links of 2024</title>
		<link href="https://spaceninja.com/blog/2024/ff-top-ten-2024/"/>
		<published>2024-12-30T00:00:00Z</published>
		<updated>2024-12-30T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/ff-top-ten-2024/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="frontend" />
    <category term="javascript" />
    <category term="css" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/feature-6f8PywBtLe-1600w.jpeg&quot;&gt;
      &lt;p&gt;Every week on &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Friday Front-End&lt;/a&gt;, I share a curated list of five articles and one video. It&#39;s fun at the end of the year to look back and see which links got the most traction. This year, I stopped posting on Twitter, and started posting on &lt;a href=&quot;https://bsky.app/profile/fridayfrontend.com&quot;&gt;Bluesky&lt;/a&gt; instead. I don&#39;t have any useful analytics from either, but here are the links that got the most clicks in the &lt;a href=&quot;https://fridayfrontend.curated.co/&quot;&gt;newsletter&lt;/a&gt; and &lt;a href=&quot;https://hachyderm.io/@fridayfrontend&quot;&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.letsbuildui.dev/articles/what-is-css-motion-path/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/css-motion-path.jpg&quot; alt=&quot;What is CSS Motion Path?&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;10: &lt;a href=&quot;https://www.letsbuildui.dev/articles/what-is-css-motion-path/&quot;&gt;What is CSS Motion Path?&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Quite simply, CSS Motion Path is a CSS property used to animate an element along a path. This path can be anything - one you&#39;ve created yourself or taken from an existing design. The element you want to animate can also be anything - HTML or even another SVG. In the past, you needed complicated math or a even Javascript library to handle animating elements in this way. Now it can be done with a few lines of CSS. Let’s find out how!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.aleksandrhovhannisyan.com/blog/the-perfect-theme-switch/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/theme-switch.jpg&quot; alt=&quot;The Perfect Theme Switch Component&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;9: &lt;a href=&quot;https://www.aleksandrhovhannisyan.com/blog/the-perfect-theme-switch/&quot;&gt;The Perfect Theme Switch Component&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;We’ll take a progressively enhanced approach, first supporting light and dark themes with CSS alone and then adding a few lines of JavaScript to allow users to select their preferred theme.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://nuejs.org/blog/tailwind-misinformation-engine/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/tailwind-messaging-pillars.jpg&quot; alt=&quot;Tailwind Marketing and Misinformation Engine&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;8: &lt;a href=&quot;https://nuejs.org/blog/tailwind-misinformation-engine/&quot;&gt;Tailwind Marketing and Misinformation Engine&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;My guess: It&#39;s only a matter of time before Tailwind collapses. The vendor-specific language and the misleading communication cannot hold water very long. The utility soup produced today will eventually turn into a technical debt. The next generation looks back and asks: &amp;quot;You actually wrote that?&amp;quot; Learn to write clean HTML and CSS and stay relevant for years to come.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://andrewwalpole.com/blog/the-showy-hidey-nav-bar/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/the-showy-hidey-nav-bar.png&quot; alt=&quot;The Showy / Hidey Navigation Bar&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;7: &lt;a href=&quot;https://andrewwalpole.com/blog/the-showy-hidey-nav-bar/&quot;&gt;The Showy / Hidey Navigation Bar&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;That’s definitely the technical term for it. It’s that popular navbar pattern where the header disappears above the screen as you scroll down, affording you more content consumption space, but then, as if it were waiting just out of view for you, reappears upon the slightest scroll up.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://nuejs.org/blog/tailwind-vs-semantic-css/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/markup-big.png&quot; alt=&quot;Tailwind vs Semantic CSS&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;6: &lt;a href=&quot;https://nuejs.org/blog/tailwind-vs-semantic-css/&quot;&gt;Tailwind vs Semantic CSS&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This study compares two websites with identical design: the commercial Spotlight template from developers of Tailwind vs the same site with semantic CSS. The gist: Semantic version is 8× smaller, renders faster, and requires no JavaScript bundlers/tooling.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.css-weekly.com/transition-to-height-auto-display-none-using-pure-css&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/transition-auto.jpg&quot; alt=&quot;Transition to height: auto &amp; display: none Using Pure CSS&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;5: &lt;a href=&quot;https://blog.css-weekly.com/transition-to-height-auto-display-none-using-pure-css&quot;&gt;Transition to &lt;code&gt;height: auto&lt;/code&gt; &amp;amp; &lt;code&gt;display: none&lt;/code&gt; Using Pure CSS&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Find out how to easily transition to intrinsic sizes and trigger transitions when an element receives its first style update using new CSS features.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.frankmtaylor.com/2024/06/20/a-rant-about-front-end-development/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/raven-keyboard.jpg&quot; alt=&quot;A Rant about Front-end Development&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;4: &lt;a href=&quot;https://blog.frankmtaylor.com/2024/06/20/a-rant-about-front-end-development/&quot;&gt;A Rant about Front-end Development&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Yes. I am absolutely calling React a problem. I’m calling Angular, Vue, and All the Rest of Them™ a problem. Because unless you have a &lt;em&gt;specific problem of highly interactive, data-driven content&lt;/em&gt;, you don’t need a framework. &lt;em&gt;You don’t need a framework to render static content to the end user. Stop creating complex solutions to simple problems.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://web.dev/articles/5-css-snippets-every-front-end-developer-should-know-in-2024&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/has-meme.jpg&quot; alt=&quot;Five CSS snippets every front-end developer should know in 2024&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;3: &lt;a href=&quot;https://web.dev/articles/5-css-snippets-every-front-end-developer-should-know-in-2024&quot;&gt;Five CSS snippets every front-end developer should know in 2024&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Toolbelt worthy, powerful, and stable CSS you can use today. I believe every front-end developer should know &lt;code&gt;:has()&lt;/code&gt; is more than a &amp;quot;parent selector&amp;quot;, the how and why of a subgrid, how to nest with built-in CSS syntax, how to let the browser balance headline text wrapping, and how use container query units.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://frontendmasters.com/guides/front-end-handbook/2024/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/frontend-dev-handbook.jpg&quot; alt=&quot;The Front End Developer/Engineer Handbook 2024&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;2: &lt;a href=&quot;https://frontendmasters.com/guides/front-end-handbook/2024/&quot;&gt;The Front End Developer/Engineer Handbook 2024&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;This handbook is a resource for both seasoned professionals and newcomers in the field of front-end web development to learn and explore the practice of front-end development.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.baldurbjarnason.com/2024/the-deskilling-of-web-dev-is-harming-us-all/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/deskilling.jpg&quot; alt=&quot;The deskilling of web dev is harming the product but, more importantly, it&#39;s damaging our health – this is why burnout happens&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;1: &lt;a href=&quot;https://www.baldurbjarnason.com/2024/the-deskilling-of-web-dev-is-harming-us-all/&quot;&gt;The deskilling of web dev is harming the product but, more importantly, it&#39;s damaging our health – this is why burnout happens&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Of course you’re having problems keeping up with everything that’s happening in web dev. Of course! You’re expected to follow half-a-dozen different specialities, each relatively fast-paced and complex in its own right, and you’re supposed to do it without cutting into the hours where you do actual paid web development.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here&#39;s the most popular video of 2024:&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=opHu7HvFM60&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2024/23-must-know-css-features.jpg&quot; alt=&quot;23 CSS features you should know &#92;(and be using&#92;) by now&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=opHu7HvFM60&quot;&gt;23 CSS features you should know (and be using) by now&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;As much as I love talking about cool new CSS features, there are a ton of super useful features that have fantastic browser support that go under the radar. So today, it&#39;s time to go over a bunch of those and I&#39;m joined by my good buddy Adam Argyle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want to enjoy more great links like this in 2025? &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Subscribe to the Friday Front-End newsletter&lt;/a&gt;, or follow Friday Front-End on &lt;a href=&quot;https://bsky.app/profile/fridayfrontend.com&quot;&gt;Bluesky&lt;/a&gt; or &lt;a href=&quot;https://hachyderm.io/@fridayfrontend&quot;&gt;Mastodon&lt;/a&gt;!&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>My Modified Couch to 5K Running Program</title>
		<link href="https://spaceninja.com/blog/2024/couck-to-5k/"/>
		<published>2024-09-08T00:00:00Z</published>
		<updated>2024-09-08T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/couck-to-5k/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="fitness" />
    <category term="running" />
    <category term="c25k" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/running-tKN_qc97AS-1600w.jpeg&quot;&gt;
      &lt;p&gt;This year, I started running again. I ran cross-country in high school, competing in the 3K and 5K races, but that was 30 years and 100 pounds ago. When I decided I wanted to start running again, I knew I&#39;d need a plan to gradually build up from basically inactive to running around 30 minutes.&lt;/p&gt;
&lt;p&gt;I was thrilled when I found &lt;a href=&quot;https://c25k.com/&quot;&gt;Couch to 5K&lt;/a&gt;, which seemed ideal. It&#39;s an &lt;a href=&quot;https://c25k.com/c25k_plan/&quot;&gt;eight week running program&lt;/a&gt; that&#39;s designed for absolute beginners to build you up to where you could complete a 5K race on week nine.&lt;/p&gt;
&lt;p&gt;The program has you run three days a week, and incorporates a five-minute warm-up and cool-down walk, which is really helpful to avoid injuries. In week one, you alternate running intervals of 60 seconds with 90 seconds of walking. Even just a minute of running was a struggle, but it was achievable. In week, two, the intervals increased to 90 seconds, and by week four it had me doing three to five minute intervals. This gradual ramp-up was really helpful both in terms of training my body, but also building mental confidence.&lt;/p&gt;
&lt;p&gt;However, I found that weeks five and six seemed wildly aggressive. Where every previous week has you running the same program all three days, in weeks five and six, every day is different. I was concerned, because I&#39;d already noticed a pattern where the first day of the week with a new plan seemed like a big step up that I could barely complete, on day two I had an easier time psychologically, because I &lt;em&gt;knew&lt;/em&gt; I could complete it. I became increasingly concerned that I was setting myself up for failure.&lt;/p&gt;
&lt;p&gt;Annie reminded me that I&#39;m not &lt;em&gt;actually&lt;/em&gt; trying to compete in a race on week nine, so there&#39;s no real schedule, and I can just shuffle things around or repeat runs as I see fit, to keep my motivation high. Game changer! So with that said, here&#39;s my modified version of the official Couch to 5K running program, which adds two weeks, shuffles the runs around a bit, and ensures that I do every run at least twice for the psychological boost.&lt;/p&gt;
&lt;table class=&quot;small&quot;&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th&gt;Week&lt;/th&gt;
      &lt;th&gt;Workout 1&lt;/th&gt;
      &lt;th&gt;Workout 2&lt;/th&gt;
      &lt;th&gt;Workout 3&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;1&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 60 seconds of jogging and
        90 seconds of walking for a total of 20 minutes.
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 60 seconds of jogging and
        90 seconds of walking for a total of 20 minutes.
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 60 seconds of jogging and
        90 seconds of walking for a total of 20 minutes.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;2&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 90 seconds of jogging and
        two minutes of walking for a total of 20 minutes.
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 90 seconds of jogging and
        two minutes of walking for a total of 20 minutes.
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk. Then alternate 90 seconds of jogging and
        two minutes of walking for a total of 20 minutes.
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;3&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then do two repetitions of the following:
        &lt;ul&gt;
          &lt;li&gt;Jog 90 Seconds&lt;/li&gt;
          &lt;li&gt;Walk 90 Seconds&lt;/li&gt;
          &lt;li&gt;Jog 3 Minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 Minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then do two repetitions of the following:
        &lt;ul&gt;
          &lt;li&gt;Jog 90 Seconds&lt;/li&gt;
          &lt;li&gt;Walk 90 Seconds&lt;/li&gt;
          &lt;li&gt;Jog 3 Minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 Minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then do two repetitions of the following:
        &lt;ul&gt;
          &lt;li&gt;Jog 90 Seconds&lt;/li&gt;
          &lt;li&gt;Walk 90 Seconds&lt;/li&gt;
          &lt;li&gt;Jog 3 Minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 Minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;4&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 2.5 minutes&lt;/li&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 2.5 minutes&lt;/li&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 2.5 minutes&lt;/li&gt;
          &lt;li&gt;Jog 3 minutes&lt;/li&gt;
          &lt;li&gt;Walk 90 seconds&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;5&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 5, day 1&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 5, day 1&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 6, day 1&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;6&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 5 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 6, day 1&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
          &lt;li&gt;Walk 5 minutes&lt;/li&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 5, day 2&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
          &lt;li&gt;Walk 5 minutes&lt;/li&gt;
          &lt;li&gt;Jog 8 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 5, day 2&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;7&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 10 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 10 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 6, day 2&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then:
        &lt;ul&gt;
          &lt;li&gt;Jog 10 minutes&lt;/li&gt;
          &lt;li&gt;Walk 3 minutes&lt;/li&gt;
          &lt;li&gt;Jog 10 minutes&lt;/li&gt;
        &lt;/ul&gt;
        &lt;small&gt;&lt;em&gt;Originally week 6, day 2&lt;/em&gt;&lt;/small&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then jog for 20 minutes with no walking.
        &lt;p&gt;&lt;small&gt;&lt;em&gt;Originally week 5, day 3&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;8&lt;/th&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then jog for 20 minutes with no walking.
        &lt;p&gt;&lt;small&gt;&lt;em&gt;Originally week 5, day 3&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then jog 22 minutes with no walking.
        &lt;p&gt;&lt;small&gt;&lt;em&gt;Originally week 6, day 3&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
      &lt;/td&gt;
      &lt;td&gt;
        Brisk five-minute warmup walk, then jog 22 minutes with no walking.
        &lt;p&gt;&lt;small&gt;&lt;em&gt;Originally week 6, day 3&lt;/em&gt;&lt;/small&gt;&lt;/p&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;9&lt;/th&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 25 minutes.&lt;/td&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 25 minutes.&lt;/td&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 25 minutes.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;10&lt;/th&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 28 minutes&lt;/td&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 28 minutes&lt;/td&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 28 minutes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;11&lt;/th&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 30 minutes&lt;/td&gt;
      &lt;td&gt;Brisk five-minute warmup walk, then jog 30 minutes&lt;/td&gt;
      &lt;td&gt;
        The final workout! Congratulations! Brisk five-minute warmup walk, then
        jog 30 minutes
      &lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

    </content>
	</entry>
	<entry>
		<title>Write Alt Text Like You’re Talking To A Friend</title>
		<link href="https://spaceninja.com/blog/2024/alt-text/"/>
		<published>2024-04-22T00:00:00Z</published>
		<updated>2024-04-22T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/alt-text/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="accessibility" />
    <category term="batman" />
    <category term="heists" />
    <category term="race" />
    <category term="alt" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/alt-text-cover-XJOE33VuZ1-1600w.jpeg&quot;&gt;
      &lt;p&gt;If you take nothing else away from this post, I want you to remember this: &lt;strong&gt;Write alternative text as if you’re describing the image to a friend.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I find people often get too wrapped up in what the “rules” are for alternative text. Sure, there are lots of things to be aware of, but almost all of them are covered under this simple guideline. If you were talking to a friend on the phone* and wanted to describe &lt;a href=&quot;https://knowyourmeme.com/photos/234739-i-have-no-idea-what-im-doing&quot;&gt;a meme you saw&lt;/a&gt;, you might say “There was this dog wearing safety glasses, surrounded by chemistry equipment, saying ‘I have no idea what I’m doing.’”&lt;/p&gt;
&lt;p&gt;Keep it brief, but informative! Give the most important information and leave out unimportant details.&lt;/p&gt;
&lt;p&gt;*I know, I know, no one actually &lt;em&gt;talks&lt;/em&gt; on the phone anymore. If the very idea is stressing you out, you can replace “describing a meme over the phone” with “describing a meme to your getaway driver as you flee the scene of your latest heist, pursued by the detective that’s been hot on your heels since the job in Naples, so she’s understandably stressed and doesn’t want to look at a funny dog picture on your phone while she’s driving.”&lt;/p&gt;
&lt;h2&gt;Context matters&lt;/h2&gt;
&lt;p&gt;Of course, context informs your description. If I was describing the chemistry dog meme to a chemistry major, I might want to emphasize all the things the dog is doing wrong. If I was describing it to someone who works in a hazardous spill response team, I might mention that the dog is pouring one of the mystery fluids into a coffee mug. As an example, let’s consider this image from a Batman movie.&lt;/p&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/alt-text/batsignal.jpg&quot; alt=&quot;A frame from Christopher Nolan’s Batman movies of Commissioner Gordon standing on the rooftop preparing to light the batsignal.&quot;&gt;
&lt;p&gt;A fan-run Batman wiki, where the audience is likely familiar with the characters, might use this as alternative text:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Commissioner Gordon, wearing his signature trenchcoat, stands near the batsignal, considering whether to light it, alerting Batman.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In contrast, an article about cinematography, where the author chose this image as an example of a framing technique and Batman isn’t the primary focus, might highlight other features of the image.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In this shot, director Christopher Nolan has framed Commissioner Gordon, played by Gary Oldman, pensively looking away from the unlit batsignal towards the sky, with the city visible behind him, reminding the viewer of the stakes inherent in his decision to summon Batman.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And if Batman had a social media account, he might take a slightly different approach.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gordon looking goofy AF after I dipped out while he was talking again LOL&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;All of these are valid alternative text choices for audiences in certain contexts.&lt;/p&gt;
&lt;h2&gt;You don’t need to say it’s an image&lt;/h2&gt;
&lt;p&gt;Most screenreaders will say “image” or “graphic” before reading the alternative text, so starting with “Image of X” is redundant. The only time you need to mention the image itself is if the medium matters, such as artwork and diagrams. For example, “a charcoal sketch of a cute kitten,” “the blueprints for the mansion we’re going to rob,” or “A chart showing a 50% decline in sales over three years.”&lt;/p&gt;
&lt;h2&gt;But you should include punctuation&lt;/h2&gt;
&lt;p&gt;Eric Bailey reminds us to &lt;a href=&quot;https://thoughtbot.com/blog/add-punctuation-to-your-alt-text&quot;&gt;add punctuation to our alternative text&lt;/a&gt;. If your alternative is only a single sentence, it might feel strange to include punctuation. But remember that it won’t be read in isolation, it will be read along with the surrounding text. Ending your sentence with a proper period or other punctuation will communicate to the screen reader how to transition from the alternative text to the following text.&lt;/p&gt;
&lt;h2&gt;Should alternative text describe race?&lt;/h2&gt;
&lt;p&gt;Sometimes! Like everything, it’s contextual. I highly recommend reading “&lt;a href=&quot;https://ux.shopify.com/the-case-for-describing-race-in-alternative-text-attributes-a093380634f2&quot;&gt;The case for describing race in alternative text attributes&lt;/a&gt;” by Tolu Adegbite, and “&lt;a href=&quot;https://tink.uk/thoughts-on-skin-tone-and-text-descriptions.md-notes-on-synthetic-speech/&quot;&gt;Thoughts on skin tone and text descriptions&lt;/a&gt;” by Léonie Watson. They both point out that by not mentioning race, we may be unintentionally reinforcing the idea that the unspoken default is white.&lt;/p&gt;
&lt;p&gt;Think of it the same way we encourage the adoption of gender pronouns in profiles even for cisgender people. It’s not about whether anyone might be confused about what your pronouns are. It’s about normalizing the idea that everyone has pronouns and they may not match your expectations.&lt;/p&gt;
&lt;h2&gt;Decorative images don’t need alternative text, but your image probably isn’t decorative.&lt;/h2&gt;
&lt;p&gt;It’s true that purely decorative images are allowed to use an empty string for their &lt;code&gt;alt&lt;/code&gt; attribute. However, as Eric Bailey points out, &lt;a href=&quot;https://www.smashingmagazine.com/2021/06/img-alt-attribute-alternate-description-decorative/&quot;&gt;your image is probably not decorative&lt;/a&gt;. In a nutshell, the term “decorative” means the image &lt;em&gt;does not visually communicate information&lt;/em&gt;, not that it is used as decoration.&lt;/p&gt;
&lt;p&gt;A spacer GIF is decorative. Image borders are decorative. A button that only contains an icon image is &lt;em&gt;not&lt;/em&gt; decorative. This photo of the safe that we’ll be cracking in the mansion is &lt;em&gt;not&lt;/em&gt; decorative. A company’s logo is &lt;em&gt;not&lt;/em&gt; decorative.&lt;/p&gt;
&lt;h3&gt;What about a person’s avatar, displayed next to their name?&lt;/h3&gt;
&lt;p&gt;This is a tricky case. If you have a person’s photo displayed right next to their name, and the alternative text only contains their name, then the screen reader will hear the person’s name twice, which adds no value. For example, at the top of this page, you can see my avatar next to my name. We’ve opted to leave the &lt;code&gt;alt&lt;/code&gt; attribute empty on the avatar, because we don’t want screen reader users to hear “Image, Scott Vandehey, Link, Scott Vandehey.”&lt;/p&gt;
&lt;p&gt;But… it’s not quite that simple. It depends on what the image shows. In “&lt;a href=&quot;https://jakearchibald.com/2021/great-alt-text/&quot;&gt;Writing great alt text: Emotion matters&lt;/a&gt;,” Jake Archibald makes the case that an avatar photo of himself on a conference site that showed him hiding behind a plant actually contained information that &lt;em&gt;should&lt;/em&gt; be expressed in the alternative text, and opted for “Jake Archibald hiding behind a plant.”&lt;/p&gt;
&lt;p&gt;As usual, context matters, and when in doubt, try reaching out to real users of assistive technology for opinions.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I know I started out by saying you only need to remember one guideline, and then gave you, like, seven. But I stand by what I said. Don’t stress about crafting the perfect alternative text. Just write it the way you would describe the photo of the bag of glistening diamonds you just stole from the Duke of Chauntelburry as you drive along the coast after finally shaking the detective, the wind blowing through your hair, with your getaway driver still chuckling about that dog-doing-chemistry meme you described to her earlier.&lt;/p&gt;
&lt;h2&gt;Learn More:&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Perkins School for the Blind, “&lt;a href=&quot;https://www.perkins.org/resource/how-write-alt-text-and-image-descriptions-visually-impaired/&quot;&gt;How to Write Alt Text and Image Descriptions for the visually impaired&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;The A11y Project, “&lt;a href=&quot;https://www.a11yproject.com/posts/are-you-making-these-five-mistakes-when-writing-alt-text/&quot;&gt;Are You Making These Five Mistakes When Writing Alt Text?&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Axess Lab, “&lt;a href=&quot;https://axesslab.com/alt-texts/&quot;&gt;Alt-texts: The Ultimate Guide&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Bureau of Internet Accessibility, “&lt;a href=&quot;https://www.boia.org/blog/8-common-image-alt-text-mistakes-to-stop-making&quot;&gt;8 Common Image Alt Text Mistakes to Stop Making&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Carie Fisher, “&lt;a href=&quot;https://www.smashingmagazine.com/2020/05/accessible-images/&quot;&gt;Accessible Images For When They Matter Most&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Steve Faulkner, “&lt;a href=&quot;https://codepen.io/stevef/pen/XWQWgrj&quot;&gt;The Perils of Using Double Quotes Inside Alt Text&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Geoff Graham, “&lt;a href=&quot;https://css-tricks.com/just-how-long-should-alt-text-be/&quot;&gt;Just How Long Should Alt Text Be?&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Stefan Judis, “&lt;a href=&quot;https://www.stefanjudis.com/today-i-learned/css-content-accepts-alternative-text/&quot;&gt;The CSS ‘content’ Property Accepts Alternative Text&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Shawn Lauriat, “&lt;a href=&quot;https://www.youtube.com/watch?v=gHOYghYYNIM&quot;&gt;How Learning ASL Improved My Alt Text&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Veronica Lewis, “&lt;a href=&quot;https://veroniiiica.com/seven-myths-about-alt-text/&quot;&gt;Seven Myths About Alt Text&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Elaina Natario, “&lt;a href=&quot;https://thoughtbot.com/blog/alt-vs-figcaption&quot;&gt;Alt vs Figcaption&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Scott O’Hara, “&lt;a href=&quot;https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html&quot;&gt;Contextually Marking Up Accessible Images and SVGs&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Adrian Roselli, “&lt;a href=&quot;https://adrianroselli.com/2024/04/long-alt.html&quot;&gt;Long Alt&lt;/a&gt;”&lt;/li&gt;
&lt;li&gt;Wren, “&lt;a href=&quot;https://mannequinrentals.help/2024/03/21/an-attempted-guide-to-writing-effective-alt-and-descriptive-text-for-art/&quot;&gt;An Attempted Guide to Writing Effective Alt and Descriptive Text for Art&lt;/a&gt;”&lt;/li&gt;
&lt;/ul&gt;

    </content>
	</entry>
	<entry>
		<title>The Many, Confusing File System APIs</title>
		<link href="https://spaceninja.com/blog/2024/file-system-apis/"/>
		<published>2024-04-02T00:00:00Z</published>
		<updated>2024-04-02T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/file-system-apis/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="development" />
    <category term="javascript" />
    <category term="standards" />
    <category term="apis" />
    <category term="filesystem" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/file-system-apis-oPKeh6Tqap-1600w.jpeg&quot;&gt;
      &lt;p&gt;There are many APIs that interact with the file system, and figuring out what they all do can be deeply frustrating. Don’t give up! I’m here to help.&lt;/p&gt;
&lt;p&gt;On a recent project, we were developing a feature that could save directories of files to the user’s file system. A coworker suggested I check out the “File System API,” but warned that it only works in Chrome. I’d never heard of it, so I searched &lt;a href=&quot;http://caniuse.com/&quot;&gt;CanIUse.com&lt;/a&gt; for “&lt;a href=&quot;https://caniuse.com/?search=file%20api&quot;&gt;file api&lt;/a&gt;.” I got a confusing list of similarly-named results, some of which appeared to be duplicates.&lt;/p&gt;
&lt;p&gt;There’s the File API, the File API (again), the File System Access API (which is marked as unofficial), the Filesystem &amp;amp; FileWriter API (also marked as unofficial), the FileReader API, the FileList API, the FileSystem API, the File API: name, the FileReader API (again), and the FileEntrySync API (marked as deprecated).&lt;/p&gt;
&lt;p&gt;Well, that wasn’t much help. So I googled “File System API” and found myself on a Chrome Developers Blog post called &lt;a href=&quot;https://developer.chrome.com/docs/capabilities/web-apis/file-system-access&quot;&gt;The File System Access API: simplifying access to local files&lt;/a&gt;. “Sounds promising,” I thought! The first thing I saw was this highlighted section:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The File System Access API—despite the similar name—is distinct from the ￼&lt;code&gt;FileSystem&lt;/code&gt;￼ interface exposed by the &lt;strong&gt;File and Directory Entries API&lt;/strong&gt;, which documents the types and operations made available by browsers to script when a hierarchy of files and directories are dragged and dropped onto a page or selected using form elements or equivalent user actions. It is likewise distinct from the deprecated &lt;strong&gt;File API: Directories and System&lt;/strong&gt; specification, which defines an API to navigate file system hierarchies and a means by which browsers may expose sandboxed sections of a user&#39;s local filesystem to web applications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Feeling intimidated, I headed to Stack Overflow, where after a bit of searching I landed on a &lt;a href=&quot;https://stackoverflow.com/questions/44094507/how-to-store-large-files-to-web-local-storage/71581910#71581910&quot;&gt;helpful answer&lt;/a&gt; that suggested I look into the Origin Private File System, but cautioned me about similarly named APIs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Don&#39;t confuse OPFS with the other filesystem and filesystem-esque APIs. MDN has a detailed rundown on &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API&quot;&gt;their &amp;quot;File System Access API&amp;quot; page&lt;/a&gt; (though I feel the page is misnamed, as it covers multiple distinct separate API surfaces, and the way it&#39;s written implies some features (like &lt;code&gt;Window.showOpenFilePicker()&lt;/code&gt;) have wide-support when the reality is quite the opposite… There’s Google&#39;s older and now deprecated (but still supported) &lt;code&gt;chrome.fileSystem&lt;/code&gt; API, originally intended for Chrome Apps and browser-extensions… There’s also the File and Directory Entries API, which has wide browser support for read-only access to the user&#39;s local computer filesystem. Note that this API is distinct from, but extends, the original W3C File API which also defines the &lt;code&gt;File&lt;/code&gt; interface.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Friends, I’m not going to lie to you. At this point, I spent a long time looking out the window and considered moving to the country to raise goats. 🐐 🐐 🐐&lt;/p&gt;
&lt;h2&gt;The APIs&lt;/h2&gt;
&lt;p&gt;Okay. Deep breath. We’re going to get to the bottom of this. The good news is that while this is confusing due to there being multiple standards with some combination of the words “file,” “system,” and “API,” there are fewer than it seems at first, and they actually build on each other, adding layers of functionality. So let’s go on a bit of a tour.&lt;/p&gt;
&lt;h3&gt;File API&lt;/h3&gt;
&lt;p&gt;Up first, we have the &lt;strong&gt;&lt;a href=&quot;https://w3c.github.io/FileAPI/&quot;&gt;File API&lt;/a&gt;&lt;/strong&gt;, a W3C&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn1&quot; id=&quot;fnref1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; draft standard&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn2&quot; id=&quot;fnref2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; for “representing file objects in web applications, as well as programmatically selecting them and accessing their data.”&lt;/p&gt;
&lt;p&gt;TL;DR: it defines what a “file” is, and allows you to read it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Using the File API, web content can ask the user to select local files and then read the contents of those files. This selection can be done by either using an HTML &lt;code&gt;&amp;lt;input type=&amp;quot;file&amp;quot;&amp;gt;&lt;/code&gt; element or by drag and drop. —&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications&quot;&gt;MDN&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What the File API does &lt;em&gt;not&lt;/em&gt; provide is any way to interact with directories or write files to the file system. Those features will be added by successive standards.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First published in October 2006&lt;/li&gt;
&lt;li&gt;Provides: &lt;code&gt;Blob&lt;/code&gt;, &lt;code&gt;File&lt;/code&gt;, &lt;code&gt;FileList&lt;/code&gt;, &lt;code&gt;FileReader&lt;/code&gt;, and the ability to create a URL from a &lt;code&gt;File&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_API&quot;&gt;MDN: File API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;FileReader API&lt;/h4&gt;
&lt;p&gt;CanIUse lists the &lt;strong&gt;&lt;a href=&quot;https://www.w3.org/TR/FileAPI/#dfn-filereader&quot;&gt;FileReader API&lt;/a&gt;&lt;/strong&gt; separately, but it’s actually part of the File API. It’s the part of the standard that allows you to actually read the contents of a file. Without this, you have access to the file’s name, size, and type, but not the content, because it’s just a blob of binary data.&lt;/p&gt;
&lt;h4&gt;File Writer API&lt;/h4&gt;
&lt;p&gt;Similar to the FileReader API, the &lt;strong&gt;&lt;a href=&quot;https://www.w3.org/TR/file-writer-api/&quot;&gt;File Writer API&lt;/a&gt;&lt;/strong&gt; was a W3C draft standard that would have extended the File API to allow writing to files from a web application. It was discontinued in 2014. I’m unable to find any reference to why, beyond some forum speculation it might have caused security problems. But don’t worry, the ability to write files was later added to the File System API (see below).&lt;/p&gt;
&lt;h4&gt;File Directories and System API&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://www.w3.org/TR/file-system-api/&quot;&gt;File Directories and System API&lt;/a&gt;&lt;/strong&gt; (listed for some reason as &lt;em&gt;Filesystem and FileWriter API&lt;/em&gt; in CanIUse) was another W3C draft standard that would have added functionality to the File API. In this case, it would have defined how to “navigate file system hierarchies, and a means to expose sandboxed sections of a user&#39;s local filesystem to web applications.” It was also discontinued in 2014, and I’m also unable to find any references to why, though the bulk of what it proposed was later recycled into the File and Directory Entries API community proposal (see below).&lt;/p&gt;
&lt;h3&gt;File and Directory Entries API&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://wicg.github.io/entries-api/&quot;&gt;File and Directory Entries API&lt;/a&gt;&lt;/strong&gt;, is a WICG&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn3&quot; id=&quot;fnref3&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; proposal&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn4&quot; id=&quot;fnref4&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; that “documents the types and operations made available by web browsers to script when a hierarchy of files and directories are dragged and dropped onto a page or selected using form elements, or equivalent user actions.” It is heavily based on the now-discontinued File Directories &amp;amp; System API.&lt;/p&gt;
&lt;p&gt;TL;DR: it extends the File API to understand how to read directories as well as files.&lt;/p&gt;
&lt;p&gt;Confusingly, the MDN pages for this API talk about a sandboxed file system, even though this proposal does not cover that (see Origin Private File System below). I believe that’s because the sandboxed file system &lt;em&gt;was&lt;/em&gt; included in the now-deprecated File Directories and System API that this was based on. Perhaps the MDN pages were originally written for that standard.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First published in May 2016&lt;/li&gt;
&lt;li&gt;Provides: &lt;code&gt;FileSystem&lt;/code&gt;, &lt;code&gt;FileSystemEntry&lt;/code&gt;, &lt;code&gt;FileSystemFileEntry&lt;/code&gt;, &lt;code&gt;FileSystemDirectoryEntry&lt;/code&gt;, and &lt;code&gt;FileSystemDirectoryReader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_and_Directory_Entries_API&quot;&gt;MDN: File and Directory Entries API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;File System API&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://fs.spec.whatwg.org/&quot;&gt;File System API&lt;/a&gt;&lt;/strong&gt; is a WHATWG&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn5&quot; id=&quot;fnref5&quot;&gt;5&lt;/a&gt;&lt;/sup&gt; living standard&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fn6&quot; id=&quot;fnref6&quot;&gt;6&lt;/a&gt;&lt;/sup&gt; that “defines fundamental infrastructure for file system APIs. In addition, it defines an API that makes it possible for websites to get access to a file system directory without having to first prompt the user for access.” Also, it “provides access to a special kind of file that is highly optimized for performance” in web workers.&lt;/p&gt;
&lt;p&gt;TL;DR: it creates a “bucket file system” (also known as the Origin Private File System, see below), allows you to access files in a file system, and allows high-performance file access for web workers.&lt;/p&gt;
&lt;p&gt;Although this standard is very new, having only been created in 2022, it actually represents a migration of parts of the older File System Access API community proposal (see below). The parts that became this document are now an approved standard, while the parts the remained behind in the community proposal are still being worked on.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First published in February 2022&lt;/li&gt;
&lt;li&gt;Provides: &lt;code&gt;FileSystemHandle&lt;/code&gt;, &lt;code&gt;FileSystemFileHandle&lt;/code&gt;, &lt;code&gt;FileSystemDirectoryHandle&lt;/code&gt;, &lt;code&gt;FileSystemWritableFileStream&lt;/code&gt;, &lt;code&gt;FileSystemSyncAccessHandle&lt;/code&gt;, and &lt;code&gt;StorageManager.getDirectory()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API&quot;&gt;MDN: File System API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Origin Private File System&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://fs.spec.whatwg.org/#sandboxed-filesystem&quot;&gt;Origin Private File System&lt;/a&gt;&lt;/strong&gt; (OPFS) is defined in the File System API. It is a sandboxed storage endpoint private to the origin of the page (meaning each web application has sandboxed storage) and not visible to the user. The standard says, “This enables use cases where a website wants to save data to disk before a user has picked a location to save to, without forcing the website to use a completely different storage mechanism with a different API for such files.”&lt;/p&gt;
&lt;p&gt;TL;DR: it provides a sandboxed private file system for your web application without touching the user’s file system.&lt;/p&gt;
&lt;p&gt;In our case, as we downloaded files from a directory on the server, we would store them in the OPFS until everything was ready, and then we would prompt the user to give us permission to copy those files from the OPFS to their file system.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system&quot;&gt;MDN: Origin Private File System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://web.dev/articles/origin-private-file-system&quot;&gt;Web.dev: The origin private file system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://webkit.org/blog/12257/the-file-system-access-api-with-origin-private-file-system/&quot;&gt;WebKit: The File System API with Origin Private File System&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;File System Access API&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://wicg.github.io/file-system-access/&quot;&gt;File System Access API&lt;/a&gt;&lt;/strong&gt; is a WICG proposal that “extends the File System API to interact with files on the user’s local device. It builds on the File API for file reading capabilities, and adds new methods to enable modifying files, as well as working with directories.”&lt;/p&gt;
&lt;p&gt;TL;DR: it extends the existing APIs to finally allow for saving to the user’s file system and improves the ability to work with directories.&lt;/p&gt;
&lt;p&gt;Originally, this proposal also contained the definitions for file system handles as well as the OPFS endpoint, but those portions were moved to the File System API in 2022, leaving behind the &lt;code&gt;Picker&lt;/code&gt; methods as a proposal.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First published in March 2016&lt;/li&gt;
&lt;li&gt;Portions were migrated to the File System API in February 2022&lt;/li&gt;
&lt;li&gt;Provides: &lt;code&gt;showOpenFilePicker()&lt;/code&gt;, &lt;code&gt;showSaveFilePicker()&lt;/code&gt;, and &lt;code&gt;showDirectoryPicker()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Learn more: &lt;a href=&quot;https://developer.chrome.com/docs/capabilities/web-apis/file-system-access&quot;&gt;Chrome: The File System Access API: simplifying access to local files&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Can I actually use any of these APIs?&lt;/h2&gt;
&lt;p&gt;That’s an excellent question, and not a simple one to answer. Normally, &lt;a href=&quot;https://caniuse.com/&quot;&gt;CanIUse.com&lt;/a&gt; is our friend, but because all of these standards are at various points in the approval process, they enjoy varying degrees of browser support (including between features in the same standard!). CanIUse solved this problem by listing individual features of each standard separately. As a result, I recommend searching for the actual feature you want, such as &lt;a href=&quot;https://caniuse.com/mdn-api_window_showdirectorypicker&quot;&gt;&lt;code&gt;showDirectoryPicker()&lt;/code&gt;&lt;/a&gt;, rather than the standard as a whole.&lt;/p&gt;
&lt;p&gt;In the case of our client app, I found the older standards were quite well supported, but the newer features provided by the File System Access API were mostly only supported in Chromium browsers. This will only improve over time, but for now, you should be sure to test your work carefully across all browsers you need to support.&lt;/p&gt;
&lt;h2&gt;Why is this so complicated?&lt;/h2&gt;
&lt;p&gt;Interacting with file systems is inherently complex. The browser needs to support actions ranging from “download a single file,” to “drop a folder containing a nested hierarchy of files and folders,” and applications ranging from “a paint program that can open an image, make edits, and save the changes,” to “a complete database in the browser.”&lt;/p&gt;
&lt;p&gt;There are also security concerns. Should the browser be able to write to any folder on the computer? (Probably not.) Should the browser allow the user to unintentionally expose sensitive system files to possibly nefarious web applications? (Arguably not. &lt;a href=&quot;https://stackoverflow.com/questions/62153191/how-does-chromium-define-a-system-file&quot;&gt;Chrome won’t let the user select certain folders&lt;/a&gt; for this reason.) Does exposing the available disk space to the web application qualify as a security risk? (Yes, &lt;a href=&quot;https://storage.spec.whatwg.org/#usage-and-quota&quot;&gt;the standard says this can lead to fingerprinting&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;In my case—trying to save a directory of files from a server—some of the restrictions seemed like overkill. After digging into the standards a bit more, I have a greater appreciation for the complexity browser makers face.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;To recap, the &lt;strong&gt;File API&lt;/strong&gt; added the ability to read a file. The &lt;strong&gt;File and Directory Entries API&lt;/strong&gt; adds the ability to read directories. The &lt;strong&gt;File System API&lt;/strong&gt; adds the &lt;strong&gt;Origin Private File System&lt;/strong&gt;, and the concept of “handles” that represent file system entries. Finally, the &lt;strong&gt;File System Access API&lt;/strong&gt; adds new “picker” methods to prompt the user for access to their file system.&lt;/p&gt;
&lt;p&gt;When I started down this path, I spent a long time trying to make sense of all these APIs with confusingly similar names. Some were deprecated, some said they only affected drag-and-drop operations, and they were all maintained by different groups. My typical destinations to learn more, CanIUse and MDN, were less helpful than usual and contained misleading or confusing information.&lt;/p&gt;
&lt;p&gt;But my confusion is your gain. Hopefully, this post will save some other developers from abandoning the web for goat farming!&lt;/p&gt;
&lt;h2&gt;Footnotes&lt;/h2&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is the W3C?&lt;/strong&gt; &lt;a href=&quot;https://www.w3.org/&quot;&gt;The World Wide Web Consortium&lt;/a&gt; develops standards and guidelines for the web. It was founded in 1994 by the inventor of the web, Sir Tim Berners-Lee. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn2&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is a draft standard?&lt;/strong&gt; W3C specifications go through &lt;a href=&quot;https://www.w3.org/standards/types/&quot;&gt;several stages&lt;/a&gt;, including: &lt;em&gt;draft standards&lt;/em&gt;, which are still evolving; &lt;em&gt;candidate standards&lt;/em&gt;, which are more mature and close to approval; and &lt;em&gt;standards&lt;/em&gt; (also known as &lt;em&gt;recommendations&lt;/em&gt;), which have been formally reviewed and endorsed by the W3C. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref2&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn3&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is the WICG?&lt;/strong&gt; &lt;a href=&quot;https://wicg.io/&quot;&gt;The Web Incubator Community Group&lt;/a&gt; is a W3C community group that incubates new web platform features. It is not a standards body, but sometimes the proposals they create become standards. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref3&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn4&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is a proposal?&lt;/strong&gt; A &lt;a href=&quot;https://github.com/WICG/proposals&quot;&gt;WICG proposal&lt;/a&gt; is an idea for a new standard that is being discussed by the community. Once consensus is reached, the WICG may advocate for the W3C or WHATWG to adopt the proposal to become a standard. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref4&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn5&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is the WHATWG?&lt;/strong&gt; &lt;a href=&quot;https://whatwg.org/&quot;&gt;The Web Hypertext Application Technology Working Group&lt;/a&gt; is a web standards body founded by community members and browser makers in 2004 when they became concerned that the W3C was mismanaging the HTML standard. &lt;a href=&quot;https://www.w3.org/news/2019/w3c-and-the-whatwg-have-just-signed-an-agreement-to-collaborate-on-the-development-of-a-single-version-of-the-html-and-dom-specifications/&quot;&gt;In 2019, the W3C and WHATWG came to an agreement&lt;/a&gt; that the WHATWG would take over maintaining the HTML standard. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref5&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;fn6&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;&lt;strong&gt;What is a living standard?&lt;/strong&gt; As opposed to the W3C’s standards which are rigorously versioned and finalized, the WHATWG maintains &lt;a href=&quot;https://whatwg.org/faq#living-standard&quot;&gt;“living standards”&lt;/a&gt; that are continuously updated. Snapshots of the living standards are taken at regular intervals for implementers to reference. &lt;a href=&quot;https://spaceninja.com/blog/2024/file-system-apis/#fnref6&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;

    </content>
	</entry>
	<entry>
		<title>How to Create a Website and a PDF from the Same Codebase</title>
		<link href="https://spaceninja.com/blog/2024/web-to-pdf/"/>
		<published>2024-03-20T00:00:00Z</published>
		<updated>2024-03-20T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/web-to-pdf/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="css" />
    <category term="development" />
    <category term="javascript" />
    <category term="docraptor" />
    <category term="eleventy" />
    <category term="howto" />
    <category term="pdf" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/feature-web-to-pdf-zGDRs3JYiX-1600w.jpeg&quot;&gt;
      &lt;p&gt;Recently a client approached us to produce a digital version of a printed information packet. Making changes to this packet was costly and time-consuming, so they wanted to convert the sections of the packet into pages on a website, with a CMS to make updates easier. The client also wanted to retain the ability to print the whole thing, with the same design quality as the existing packet. We were able to get a lot done using CSS print styles, but because browsers don’t support the full suite of CSS print styles, it was clear we’d ultimately need to generate a PDF to get the print design we desired.&lt;/p&gt;
&lt;p&gt;This started a journey into the world of HTML-to-PDF services, and I’m quite pleased with the solution we landed on. I’ve since started using it in another project, and I wanted to share it with you today. In a nutshell, we’re generating a website from a CMS using &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; and generating a PDF version of the website using &lt;a href=&quot;https://docraptor.com/&quot;&gt;DocRaptor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To make this project easier to talk about without all the client-specific details, I’ve created an &lt;a href=&quot;https://github.com/spaceninja/eleventy-pdf&quot;&gt;example repo&lt;/a&gt;. It takes a public-domain Sherlock Holmes story and generates both a &lt;a href=&quot;https://eleventy-pdf.netlify.app/&quot;&gt;website&lt;/a&gt; and a &lt;a href=&quot;https://eleventy-pdf.netlify.app/pdf/a-study-in-scarlet.pdf&quot;&gt;PDF&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;The PDF Service&lt;/h2&gt;
&lt;p&gt;To generate the PDF, we’re using an HTML-to-PDF API service called &lt;a href=&quot;https://docraptor.com/&quot;&gt;DocRaptor&lt;/a&gt;. I found it to be easy to use and I’m happy to recommend it, but there are alternatives out there, including &lt;a href=&quot;https://developer.adobe.com/document-services/apis/pdf-services/&quot;&gt;Adobe PDF Services&lt;/a&gt;, &lt;a href=&quot;https://weasyprint.org/&quot;&gt;WeasyPrint&lt;/a&gt;, and even the tool that DocRaptor is built on, &lt;a href=&quot;https://www.princexml.com/&quot;&gt;PrinceXML&lt;/a&gt;. They all do roughly the same things, and it would be easy to swap out DocRaptor for another service.&lt;/p&gt;
&lt;p&gt;The main thing you need to understand is that we’re going to make an API call to a PDF generation service, and the body of our request will be the HTML it will use to generate the PDF. That HTML needs to include not only all the contents of the PDF but also all the images and styles.&lt;/p&gt;
&lt;p&gt;Let’s break this down into three parts: How we generate the HTML for the PDF, the JavaScript we use to generate the PDF, and the CSS we use to style both.&lt;/p&gt;
&lt;h2&gt;The HTML&lt;/h2&gt;
&lt;p&gt;The example repo uses &lt;a href=&quot;https://www.11ty.dev/&quot;&gt;Eleventy&lt;/a&gt; to generate the website. I like Eleventy a lot, and I think it’s a good fit for this kind of static site, but you could easily replace it with any build tool you wanted, including none at all. This process would work just as well with a hand-written HTML file. The only thing that matters is that we have a single HTML file that contains all the content we want to end up in our PDF.&lt;/p&gt;
&lt;p&gt;Our goal was to have both a website and a PDF. In the example repo, the website is a public domain Sherlock Holmes story, broken up with each chapter on its own page to make reading easier. But to generate the PDF, we need a single HTML file with all the content.&lt;/p&gt;
&lt;p&gt;Eleventy makes it easy to do this with &lt;a href=&quot;https://github.com/spaceninja/eleventy-pdf/blob/main/src/a-study-in-scarlet.njk&quot;&gt;a single file&lt;/a&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-njk&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-njk&quot;&gt;&lt;span class=&quot;token delimiter punctuation&quot;&gt;{%&lt;/span&gt; &lt;span class=&quot;token tag keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;title-page.njk&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;frontispiece.njk&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;contents.njk&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;chapterObject&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;chapters&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;article&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;new-page&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;chapter-header.njk&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;chapterObject&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;content&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;safe&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;article&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;endfor&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re manually including a few PDF-specific pages, such as the title page and table of contents. Then we loop over all the book content, which is in an Eleventy collection called “chapters.” We write out the contents of each chapter on the page wrapped in an &lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt; element. &lt;a href=&quot;https://eleventy-pdf.netlify.app/a-study-in-scarlet/&quot;&gt;Here’s what this all-in-one page looks like on the website&lt;/a&gt;, though it’s worth noting that no one will be looking at this page, it’s just being generated so we can pass it to the PDF generation service.&lt;/p&gt;
&lt;h2&gt;The CSS&lt;/h2&gt;
&lt;p&gt;One of the best parts of this process is that once you write the CSS for the website, you’re 90% done with the CSS for the PDF as well. DocRaptor is powered by PrinceXML under the hood, which has very good CSS support. And, since all of the DocRaptor CSS is based on &lt;a href=&quot;https://drafts.csswg.org/css-page-3/&quot;&gt;real-world specs for CSS print styles&lt;/a&gt;, almost everything you write for the PDF has the bonus of giving your website good print styles.&lt;sup class=&quot;footnote-ref&quot;&gt;&lt;a href=&quot;https://spaceninja.com/blog/2024/web-to-pdf/#fn1&quot; id=&quot;fnref1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I found that I was able to use all the CSS I wrote for the website and I only needed to add a single print stylesheet that contained some additional rules for DocRaptor such as page margins, hiding some web-only content, and adding page numbers.&lt;/p&gt;
&lt;p&gt;The only “bugs” I needed to fix in my existing CSS were related to modern syntax that DocRaptor doesn’t understand just yet, like logical properties (I had to replace a few instances of &lt;code&gt;margin-inline&lt;/code&gt; with &lt;code&gt;margin-left&lt;/code&gt; and &lt;code&gt;margin-right&lt;/code&gt;). Another common change was adding &lt;code&gt;page-break-inside: avoid&lt;/code&gt; to keep images from breaking across pages, for example.&lt;/p&gt;
&lt;p&gt;When testing CSS changes, I found that the browser’s print preview was often good enough, if I didn’t want to wait for another API call to generate an updated PDF. Another option in Chrome is to &lt;a href=&quot;https://developer.chrome.com/docs/devtools/rendering/emulate-css/&quot;&gt;emulate print media&lt;/a&gt;, which will apply the print styles in the browser window. When testing the print layout in the browser, I found that the printed page was 816 pixels wide (&lt;a href=&quot;https://docraptor.com/documentation/article/1067959-size-dimensions-orientation&quot;&gt;8.5 inches at 96 pixels per inch&lt;/a&gt;), which means my content column, after subtracting the 0.75 inches of margins, was 672 pixels wide.&lt;/p&gt;
&lt;p&gt;You can view the &lt;a href=&quot;https://github.com/spaceninja/eleventy-pdf/tree/main/src/_scss&quot;&gt;full CSS for the example site&lt;/a&gt;, but I’d like to talk about a few specific features.&lt;/p&gt;
&lt;h3&gt;Page Margins&lt;/h3&gt;
&lt;p&gt;You can easily adjust the page margins in the PDF:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@page&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.75in&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s actually standard CSS. You can read &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/@page&quot;&gt;more details on MDN&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to override the margins on a particular page you assign it a name, and update the rules for that page.&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.full-bleed-page&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;page&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; full_bleed_page&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@page&lt;/span&gt; full_bleed_page&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;margin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Page Header&lt;/h3&gt;
&lt;p&gt;Like a printed book, I wanted to put the name of the story in the header for each page. This turns out to be easy, again using standard CSS:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@page&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@top&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;A Study in Scarlet&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Merriweather&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;margin-top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;@top&lt;/code&gt; at-rule targets a section of the page appearing in the page margin itself. In our case, we’ve targeted the top section, added content, styled it, and given it a bit of margin from the top of the page. By default, the content will be centered.&lt;/p&gt;
&lt;p&gt;Note that if you’re not careful, this content could overlap your page content.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docraptor.com/documentation/article/1067094-headers-footers-for-pdfs&quot;&gt;DocRaptor docs: Headers &amp;amp; Footers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Page Numbers&lt;/h3&gt;
&lt;p&gt;Adding page numbers is a similar operation:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@page&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@bottom&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;page&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-family&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Merriweather&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; serif&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;margin-top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1em&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re adding content to the &lt;code&gt;@bottom&lt;/code&gt; section, but rather than giving it a simple string of text, we’re saying it should use the value of the page counter, which DocRaptor defines for us.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docraptor.com/documentation/article/1082618-page-numbers&quot;&gt;DocRaptor docs: Page Numbers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Table of Contents&lt;/h3&gt;
&lt;p&gt;Now the table of contents takes a little more work. In our HTML, we have a simple ordered list with jump links to the appropriate sections of the document, like so:&lt;/p&gt;
&lt;pre class=&quot;language-html&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-html&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ol&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;I&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;toc-item&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;toc-item__title&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;#ch-I-mr-sherlock-holmes&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Mr. Sherlock Holmes&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;toc-item__page print-only&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;#ch-I-mr-sherlock-holmes&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  ...
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the &lt;code&gt;.print-only&lt;/code&gt; class, which as you might guess hides that element from view in the browser. Also note the empty anchor tag it contains, which we will populate with a page number using CSS.&lt;/p&gt;
&lt;p&gt;When this block of code is shown in the browser, we get a simple unordered list of chapter titles that are jump links to further down the document, with no page numbers.&lt;/p&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/web-to-pdf/toc-browser.png&quot; alt=&quot;Screenshot of the table of contents rendered in a browser with no page numbers.&quot;&gt;
&lt;p&gt;Now, we add this CSS for the print styles:&lt;/p&gt;
&lt;pre class=&quot;language-css&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.toc-item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; flex&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.toc-item__title&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;flex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.toc-item__title::after&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;leader&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dotted&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* add dot leaders */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.toc-item__page a::after&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;target-counter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;attr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;href&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; page&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* add page numbers */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We make each table of contents item into a flex layout and assign all the space to the title.&lt;/p&gt;
&lt;p&gt;We add dot leaders using &lt;a href=&quot;https://www.princexml.com/doc/gen-content/&quot;&gt;generated content&lt;/a&gt; after the title using &lt;a href=&quot;https://www.w3.org/Style/Examples/007/leaders.en.html&quot;&gt;the proposed &lt;code&gt;leader()&lt;/code&gt; syntax&lt;/a&gt; (which at the moment, I believe is only supported by PrinceXML!)&lt;/p&gt;
&lt;p&gt;We insert the page number for the chapter using a clever bit of syntax that lets DocRaptor look up the page number that will contain the ID the jump link is targeting.&lt;/p&gt;
&lt;p&gt;And then we get dot leaders and page numbers automatically added via CSS!&lt;/p&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/web-to-pdf/toc-print.png&quot; alt=&quot;Screenshot of the table of contents rendered in a pdf with page numbers.&quot;&gt;
&lt;ul&gt;
&lt;li&gt;DocRaptor docs: &lt;a href=&quot;https://docraptor.com/documentation/article/1082618-page-numbers&quot;&gt;Page Numbers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DocRaptor docs: &lt;a href=&quot;https://docraptor.com/documentation/tutorial/table-of-contents&quot;&gt;Table of Contents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The JavaScript&lt;/h2&gt;
&lt;p&gt;Now, let’s talk about the Node script we use to submit the HTML to the PDF generation service. You can view &lt;a href=&quot;https://github.com/spaceninja/eleventy-pdf/blob/main/build-pdf.mjs&quot;&gt;the full script on GitHub&lt;/a&gt;, but I’ll walk you through the structure of what we’re doing here.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;generatePDF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;dist/a-study-in-scarlet/index.html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first thing that happens is we call the &lt;code&gt;generatePDF()&lt;/code&gt; function and pass it the path to our HTML file.&lt;/p&gt;
&lt;h3&gt;generatePDF()&lt;/h3&gt;
&lt;p&gt;The generatePDF function is our one-stop shop for generating a PDF from an HTML file, but it farms out the work to several smaller functions for ease of maintenance.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;generatePDF&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;htmlPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Get the slug and path info for this HTML file&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; meta &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMeta&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;htmlPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Get the contents of the HTML file&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; html &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHtmlFromFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;htmlPathCWD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Create a PDF from the HTML contents&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdf &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetchPDF&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slug&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Create the output directory if it doesn&#39;t exist&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;distDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Save the PDF to a file&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pdf&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;[PDF] Writing &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pdfPath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First, it calls &lt;code&gt;getMeta()&lt;/code&gt;, which returns information including the file slug and the full path info. Then it passes the HTML file path to &lt;code&gt;getHtmlFromFile()&lt;/code&gt;, which reads the actual markup from the file and makes some changes we need for DocRaptor, like inlining the CSS and images. Then it takes the markup and passes it to &lt;code&gt;fetchPDF()&lt;/code&gt;, which handles the actual API call to DocRaptor and returns PDF data. Finally, it writes the PDF data to a file.&lt;/p&gt;
&lt;p&gt;Let’s take a look at those functions in more detail.&lt;/p&gt;
&lt;h3&gt;getMeta()&lt;/h3&gt;
&lt;p&gt;Up first, we have &lt;code&gt;getMeta()&lt;/code&gt;, which accepts a path to an HTML file and returns information about that file.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getMeta&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;htmlPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Strip `dist/` and `/index.html` from htmlPath&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; slug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; htmlPath&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Special case for the root HTML file&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;htmlPath &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dist/index.html&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; slug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;home&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Create relative HTML path and PDF write destination&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; htmlPathCWD &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;currentDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; htmlPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Convert any slashes to dashes for the PDF filename&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfSlug &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; slug&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;-&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Create the PDF write destination&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;distDir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pdfSlug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.pdf&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    slug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    htmlPathCWD&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    pdfSlug&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    pdfPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We get back four pieces of information, which are all used later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;slug&lt;/code&gt;, which is the filename, is only used for error logging if something goes wrong.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;htmlPathCWD&lt;/code&gt; is the full path to the HTML file including the current working directory. We need this to read the contents of the file in the next function.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pdfSlug&lt;/code&gt; is used for the filename of the PDF, and we’re just replacing any slashes with dashes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pdfPath&lt;/code&gt; is the final output directory of the PDF, which is our &lt;code&gt;dist&lt;/code&gt; folder plus the PDF filename.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;getHtmlFromFile()&lt;/h3&gt;
&lt;p&gt;Next, we have &lt;code&gt;getHtmlFromFile()&lt;/code&gt; which is responsible not only for getting the actual contents of the HTML file but also for making changes we need for PDF generation.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; inlineAssets &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unified&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rehypeParse&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;fragment&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rehypeInline&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;use&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rehypeStringify&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getHtmlFromFile&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;htmlPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Grab the HTML file contents as a string&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rawHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;htmlPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;utf8&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Change the CSS URI to a path so it can be inlined&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; updatedHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; rawHTML&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/style.css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dist/style.css&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Change any image URLs to paths so they can be inlined&lt;/span&gt;
  updatedHTML &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; updatedHTML&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replaceAll&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;/images/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;dist/images/&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Inline the assets&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; inlineAssets&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;updatedHTML&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once it loads the HTML contents, it modifies any CSS and image URLs to file paths so they can be inlined. We inline the CSS and images because the only thing we pass to DocRaptor is the HTML itself. DocRaptor can load assets from public URLs, but during development work, none of our files were public, so we got in the habit of inlining them.&lt;/p&gt;
&lt;p&gt;For inlining, we’re using a library called &lt;a href=&quot;https://www.npmjs.com/package/rehype-inline&quot;&gt;rehype-Inline&lt;/a&gt;, which is capable of inlining CSS, JavaScript, and images in HTML documents.&lt;/p&gt;
&lt;h3&gt;fetchPDF()&lt;/h3&gt;
&lt;p&gt;Finally, we come to the meat of the process: Passing the HTML to DocRaptor, which will return a PDF.&lt;/p&gt;
&lt;pre class=&quot;language-js&quot; tabindex=&quot;0&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;fetchPDF&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slug&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;docraptorApiKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Missing DocRaptor API Key&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Send HTML to DocRaptor to generate PDF&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; pdfRes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;https://docraptor.com/docs&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;headers&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Basic &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;docraptorApiKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;base64&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token string-property property&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; docraptorTest&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;document_content&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; html&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;pdf&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;prince_options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;PDF/UA-1&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Adds accessibility features like tagging&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;pdfRes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ok&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;slug&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pdfRes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;pdfRes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;statusText&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; pdfRes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Extract the PDF from the response and return it&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; blob &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; pdfRes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;blob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Buffer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; blob&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;arrayBuffer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;binary&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a simple fetch request to the DocRaptor API. You’ll need to define a DocRaptor API key and tell it whether or not to use “test” mode, which is free, but adds an overlay. The one extra option we’re defining is asking DocRaptor to use the PDF/UA-1 profile, which adds accessibility features.&lt;/p&gt;
&lt;p&gt;This function returns the raw PDF data, which we then save to the file system, and hey presto! We have a PDF!&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I’m quite pleased with this process because each step adds to the previous ones without getting tangled up. The website doesn’t know anything about the PDF. It’s just a straightforward Eleventy site that happens to include a single-page version of the website’s contents. That website includes print styles, including a handful of rules that only work in DocRaptor, but are all based on standard or proposed CSS syntax. The PDF generation itself happens entirely in a Node script that can be updated, modified, or even replaced in the future without breaking the website.&lt;/p&gt;
&lt;p&gt;I know this probably isn’t a common problem, but I hope this article helps someone else who might be looking at a similar request and isn’t sure how to get started.&lt;/p&gt;
&lt;h2&gt;Footnotes&lt;/h2&gt;
&lt;section class=&quot;footnotes&quot;&gt;
&lt;ol class=&quot;footnotes-list&quot;&gt;
&lt;li id=&quot;fn1&quot; class=&quot;footnote-item&quot;&gt;&lt;p&gt;It’s worth noting that while the DocRaptor CSS is based on a real spec, a lot of the print-specific CSS is unsupported in most browsers. CSS Paged Media is unsupported in Safari, and has limited support in Firefox. The dot-leader styles we use for the table of contents is unsupported in &lt;em&gt;any&lt;/em&gt; browser other than PrinceXML. As a result, while you’ll get good print styles in most browsers, they certainly won’t match the quality of the PDF. If more browsers supported these standards, it’s possible we could have delivered this project without needing to generate a PDF at all. &lt;a href=&quot;https://spaceninja.com/blog/2024/web-to-pdf/#fnref1&quot; class=&quot;footnote-backref&quot;&gt;↩︎&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;

    </content>
	</entry>
	<entry>
		<title>Why I Log My Media Diet</title>
		<link href="https://spaceninja.com/blog/2024/why-i-log-my-media-diet/"/>
		<published>2024-03-18T00:00:00Z</published>
		<updated>2024-03-18T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/why-i-log-my-media-diet/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="books" />
    <category term="movies" />
    <category term="games" />
    <category term="comics" />
    <category term="television" />
    <category term="reviews" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/star-ratings-L4Hv1sEN92-1600w.jpeg&quot;&gt;
      &lt;p&gt;Annie asked me the other day if I kept journals as a kid, or in some other way recorded the books I read and movies I watched. I was stumped. I don’t recall ever doing such a thing. My mom keeps calendars where she records the events of the day and the weather, and I remember always finding this a bit odd. When would I ever need to know what the weather was like on a particular day years ago? And yet, here I am, with dedicated accounts across five different websites to track all the books and comics I read, games I play, and shows and movies I watch. How did I get here?&lt;/p&gt;
&lt;p&gt;It all started with Netflix in 2004. We were living in Puyallup, Washington, had no kids, and I’d watch several movies a week. With each, I’d give it a 1 to 5 star rating on Netflix. I didn’t particularly care about the ratings, but Netflix had a recommendation algorithm that got more accurate the more movies you rated. So I started not only rating all the movies we rented but also everything I could ever remember watching. The recommendations became startlingly good as a result.&lt;/p&gt;
&lt;p&gt;I was surprised to find a bonus reason to rate movies. I have a terrible memory. More than once, someone recommended a movie to me, I’d put it on my list, rent it, and then realize I’ve seen it before, but forgot. Now, with Netflix, I would go to put it in my queue, and see that I’d already watched it a few years ago, and rated it. “Ah, only 3 stars, that must be why I don’t remember it.” This happened many, many times.&lt;/p&gt;
&lt;p&gt;Rating books came later. I’ve always aspired to be a voracious reader. My dad devoured science fiction novels, and the walls of our house were covered with shelves of cheap paperbacks stacked two deep. But as a poor college student and then as a poor college graduate, I didn’t have a big budget for buying books. I switched to ebooks as a necessity, because they were much cheaper.&lt;/p&gt;
&lt;p&gt;I created a &lt;a href=&quot;https://www.goodreads.com/spaceninja&quot;&gt;GoodReads&lt;/a&gt; account in 2013 when I noticed that Kindle had a feature to automatically mark as read on GoodReads the book you just finished, and prompt you for a star rating. Already being in the habit of doing this for movies on Netflix, and knowing how useful it was there, I started doing the same and found it just as useful.&lt;/p&gt;
&lt;p&gt;Another thing GoodReads does that I love is their annual reading challenge. You set a public goal of how many books you want to read in the coming year (I’ve set a goal of 24 for a few years running), and it helpfully displays your progress when you visit the site. I actually find it really motivating to see “you’re one book behind schedule,” and it can help motivate me to make a little more time for reading.&lt;/p&gt;
&lt;p&gt;In 2017, Netflix announced they were switching from a five-star rating model to a simple thumbs-up/thumbs-down system, and I had a moment of panic. I had over a decade of movie ratings that were about to disappear. I went down a rabbit hole and ended up writing a blog post called &lt;a href=&quot;https://spaceninja.com/blog/2017/how-to-export-movie-ratings-from-netflix-and-import-into-imdb/&quot;&gt;How to Export Movie Ratings from Netflix and Import into IMDb&lt;/a&gt;. The end result was I preserved my ratings. I didn’t really like IMDb, though. It was good for looking movies up, but less good for keeping a record of what I watched.&lt;/p&gt;
&lt;p&gt;In 2018, I discovered &lt;a href=&quot;https://letterboxd.com/spaceninja/&quot;&gt;Letterboxd&lt;/a&gt;, which conveniently had a system to import ratings from IMDb, and I switched over. Since then, every movie I watch gets a rating on Letterboxd.&lt;/p&gt;
&lt;p&gt;In 2021, I was looking at the cool &lt;a href=&quot;https://letterboxd.com/spaceninja/year/2021/&quot;&gt;year in review&lt;/a&gt; page that Letterboxd automatically generates for you, and I realized I wanted something similar for TV Shows. I spent some time checking out the options and ended up creating an account on &lt;a href=&quot;https://trakt.tv/users/spaceninja00&quot;&gt;Trakt&lt;/a&gt;, where I now record all the TV I watch.&lt;/p&gt;
&lt;p&gt;And then I thought, if I’m recording books and TV and movies, I should probably record the video games I play. So I set up an account on &lt;a href=&quot;https://www.grouvee.com/user/120880-spaceninja/shelves/644298-played/&quot;&gt;Grouvee&lt;/a&gt;. I don’t love Grouvee, but the other options are all flawed in some way, and I finish games infrequently enough that it’s not a problem. Still, if I found something better, I’d probably switch.&lt;/p&gt;
&lt;p&gt;Most recently, a few months ago, I was complaining about how my iPad is too old to run the Marvel Unlimited app, so I have to read comics on their website, which works, but means I lost the to-read list and reading history from the app. So I started looking for where people track their comics, and now I have an account on &lt;a href=&quot;https://leagueofcomicgeeks.com/profile/spaceninja&quot;&gt;League of Comic Geeks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So… That’s how I arrived at this point. I wasn’t always a data nerd, and I still don’t think I am, really. But I do habitually log and rate every TV show, movie, video game, book, and comic that I consume.&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>Reflections on the End of a Seven-Year D&amp;D Campaign</title>
		<link href="https://spaceninja.com/blog/2024/dnd-campaign/"/>
		<published>2024-02-07T00:00:00Z</published>
		<updated>2024-02-07T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2024/dnd-campaign/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="dnd" />
    <category term="rpgs" />
    <category term="games" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/red-dragon-l1ziSQ4v30-1280w.jpeg&quot;&gt;
      &lt;p&gt;Like a lot of people, I got into D&amp;amp;D because of a liveplay podcast. Specifically, the &lt;em&gt;first&lt;/em&gt; liveplay podcast, &lt;em&gt;Acquisitions Incorporated&lt;/em&gt;, which not only kickstarted an entirely new genre of entertainment but also remained a juggernaut in the field. Anyone who wasn’t there when it started may be unaware that it began pretty humbly — Wizards of the Coast reached out to the &lt;em&gt;Penny Arcade&lt;/em&gt; guys in 2008 to ask if they’d be interested in recording a gameplay session to help promote the upcoming release of the fourth edition of D&amp;amp;D.&lt;/p&gt;
&lt;p&gt;The original team was just three players — Mike and Jerry from &lt;em&gt;Penny Arcade&lt;/em&gt; and their friend Scott from &lt;em&gt;PvP&lt;/em&gt; — and now legendary DM Chris Perkins. Their mix of experience with the game was perfect for promoting a new edition. Mike had never played any D&amp;amp;D before, while Scott and Jerry had some experience under their belts. As a result, Perkins could explain the game for newbies when talking to Mike, and get into the nitty-gritty of the changes for fourth edition when talking to Jerry and Scott. It helped that the three players were extremely funny, and used to riffing together to come up with webcomics.&lt;/p&gt;
&lt;p&gt;I was hooked, and immediately bought the starter set, and by April 2009, I had roped some friends into playing a game, with me DMing. That group met infrequently, and over the next few years, I joined some games as a player, attended some pickup games at conventions, and DM’d for several groups of coworkers across a few companies.&lt;/p&gt;
&lt;p&gt;But the longest stint started when I joined Say Media. In September of 2015, I put out word that I was interested in starting a D&amp;amp;D group, and invited anyone interested to join us. No commitment, and no need to know the rules, just show up ready to have fun. I ran a group of 5-10 coworkers through a &lt;em&gt;Tomb of Horrors&lt;/em&gt;-style adventure. We all had a good time, and I put a recurring event on the calendar: Every other Wednesday after work, anyone who wants to join is welcome, I’ll run the game as long as there are at least three players.&lt;/p&gt;
&lt;p&gt;Over the next few years, people drifted in and out, but the core of the group stayed together, continuing to meet every other week even after I lost my job at Say. We’d meet up at whoever’s office was convenient and had a conference room to use.&lt;/p&gt;
&lt;p&gt;When Covid hit, we kept playing remotely. It took a while to adjust to playing over Zoom, but we made it work. There are fewer fun opportunities for excited cross-talk, but it’s also a lot easier to follow what’s going on with mostly only one person speaking at a time.&lt;/p&gt;
&lt;p&gt;At first, I was just running pre-published adventures for the group. A mix of whatever I could find that sounded fun. At times, I would come up with my own stuff, but I had a tendency to fall back on &lt;a href=&quot;https://dnd.wizards.com/adventurers-league&quot;&gt;Adventurers League&lt;/a&gt; modules because they were quick and easy to run without much prep work.&lt;/p&gt;
&lt;p&gt;My group continually astonished me. One time we were playing in the city of Mulmaster, a canonically horrible place, and to be perfectly honest, my group wasn’t loving it. The vibe was wrong. I had a suspicion something needed to change when to prep for a heist, one player got a job as a security guard and spent some time wondering why he would ever go back to the heist rather than keeping the steady paycheck and low-stress guard job. The heist went off well, but soon after they were having a hard time chasing a fire-worshipping cultist who was starting a conflagration when one player said “Fuck it. I think we should leave.” And then the group GOT ON A BOAT and LEFT THE CITY. I had to scramble a bit and ended the session early.&lt;/p&gt;
&lt;p&gt;When they came back next time, I announced that some time had passed, they’d sailed to the city of Neverwinter and were looking at wanted posters with their faces — since they’d fled the scene of the crime, they’d been blamed for the terrible fire that burned a huge portion of Mulmaster. The group spent some time clearing their name, but eventually named themselves the Mulmaster County Volunteer Fire Department in a cheeky nod to the event.&lt;/p&gt;
&lt;p&gt;One time, I decided that we’d done plenty of dungeons, but hadn’t actually met any dragons. I spent several games leading them towards a confrontation with a dragon. This was going to be a big fight, but I thought they could handle it. The story evolved organically. If a player said something funny or clever, I’d work it in.&lt;/p&gt;
&lt;p&gt;By the time they arrived at the dragon’s lair, we’d established that the dragon’s crew of cultists had been kidnapping bards across the land, forcing them to learn new songs to convince more people to worship the dragon. The group broke in, freed the bards, and went to confront the dragon — who by this point had been established as an aspiring rapper. Then, rather than fight with weapons, they challenged the dragon to a rap battle.&lt;/p&gt;
&lt;p&gt;There are no rules in D&amp;amp;D for a rap battle. Trust me, I looked.&lt;/p&gt;
&lt;p&gt;I had to improvise a series of contested performance challenges. The group aced it. It wasn’t even close. In-game, they soundly defeated the dragon — then they offered to represent him as agents. From then on, my challenge as a DM was to find creative ways to allow them to “book a show” to summon the dragon that didn’t just amount to handing them the ability to call in an air strike. Eventually, the dragon figured out they weren’t getting him good gigs and fired them.&lt;/p&gt;
&lt;p&gt;Over time, the group got a bit too raucous. One player collected trophies. Another thought that was a good idea, and started collecting mementos from defeated enemies. A branch from a treant. The mechanical arm from a robotic construct. Over time, without fully considering it, the running joke had become the rather grisly practice of peeling the faces of the dead.&lt;/p&gt;
&lt;p&gt;One night, a new player joined our group. Someone we didn’t know in person, but I had met at a tech meetup. She started out enthusiastic to play with us and left the evening clearly horrified after watching the group burn an enemy alive, peel his face, and then commit a bit of light genocide against a group of modrons.&lt;/p&gt;
&lt;p&gt;It was a wake-up call. I remember talking to Chuck about how upset she was and how awful I felt that she had a bad time. These were all just jokes that had started small but had somehow grown into horrifying proportions. Chuck helped me realize that my discomfort had more to do with myself and realizing that I wasn’t running the kind of game I wanted to run anymore. I don’t need my players to be exemplars of justice and moral fortitude — but somehow we’d stopped even trying to be the good guys.&lt;/p&gt;
&lt;p&gt;Around this time Chuck also became a much bigger part of the story. He’d joined the group as a player a few times, but it never really clicked for him. But he loved hearing me tell him about what the group got up during the games, and acting as a sounding board for my ideas. Eventually, I realized that Chuck was more than just a silent partner, he’d become a co-DM, and I started referring to him that way. Some of the best ideas in the campaign came from riffing with Chuck, each of us helping to knock the sharp edges off rough new ideas and refine them down to something approaching a plan.&lt;/p&gt;
&lt;p&gt;What came out of our conversation was an adventure my players fondly (I hope) refer to as “the morality arc.” I had them be kidnapped into a dystopian future by a version of the History Channel and had them compete in a reality show gladiator program forcing the worst criminals from across history to fight to the death. That’s how they learned that they were regarded by the future as notorious villains. (I came up with a mini-game to generate a Wikipedia page that I’m still quite proud of.) In the end, the players returned to their own timeline, with a little in-game nudge to try to leave the world better than they found it. (Don’t worry, I also had a frank conversation with them about what I wanted to keep the game fun moving forward, and they were all on board.)&lt;/p&gt;
&lt;p&gt;What followed was me and Chuck creating a campaign structure where the group worked for a secret organization whose explicit aim was to make the world a better place. As operatives, they had a free hand to pursue this goal as they saw fit, but they would receive bonuses for non-lethal solutions.&lt;/p&gt;
&lt;p&gt;That was November 2018. What followed was four years of a single campaign of mostly published one-off adventures united by an overarching plot involving the group they worked for, the big bad the group was opposed to, and a surprisingly complicated political backstory, mostly guided by what my players responded well to.&lt;/p&gt;
&lt;p&gt;The finale of that campaign happened in February 2022. It played out over three sessions and resulted in finally paying off an idea I’d been seeding and teasing for literally years, a goddamn set of Voltron armor the group had to use to fight an elder god in the form of an ancient dragon. It was so much fun, and loaded with fan-service-y callbacks to earlier encounters and characters they’d met over the years.&lt;/p&gt;
&lt;p&gt;We’ve since started a new campaign, that I’m having a lot of fun with, but I will never forget the time I spent with this group, especially the longest-running characters.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Averlyth Cai, the drow cleric of Bane, who spent a great deal of time spreading the good word of a terrifying god, and who ended the campaign riding around in a pimped-out coffin on spider legs.&lt;/li&gt;
&lt;li&gt;Kanye Cantaliber Esq., the half-orc battle master who never met a door he didn’t kick open, carried a scroll of pedigree and always announced himself before a fight (to give his opponent a chance to flee).&lt;/li&gt;
&lt;li&gt;Nissa Atlock, the gnome bard who spent the vast majority of her time slinging insults from the safety of Kanye’s shoulders and looking for new libraries to spend time in.&lt;/li&gt;
&lt;li&gt;Quinumum Ebraldeth, aka “Um,” the halfling rogue who acquired a set of slippers of spider climbing, and spent every fight from that point forward as a sniper hanging from the ceiling like a bat.&lt;/li&gt;
&lt;li&gt;Sorith, the dwarf monk, whose love of brewing was matched only by his distaste for other dwarves.&lt;/li&gt;
&lt;li&gt;And of course, Carlos Blöodfarte, the half-elf sorcerer whose unlucky roll on a wild magic surge chart left him with a very long neck for the rest of the campaign.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I love them all like children and have threatened/promised my players to bring their characters back as NPCs in an adventure I may publish someday.&lt;/p&gt;
&lt;p&gt;When we used to play in the office, my boss would knock on the door and teasingly ask “Who’s winning?” And I would always tell him: “Me! I’m winning, because I somehow convinced my friends to show up every other week to tell me stories and make me laugh.”&lt;/p&gt;

    </content>
	</entry>
	<entry>
		<title>TV Shows I Loved in 2023</title>
		<link href="https://spaceninja.com/blog/2023/tv-shows-i-loved-in-2023/"/>
		<published>2023-12-31T00:00:00Z</published>
		<updated>2023-12-31T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2023/tv-shows-i-loved-in-2023/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="reviews" />
    <category term="television" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/strange-new-worlds-pSfClBsQ7n-1600w.jpeg&quot;&gt;
      &lt;p&gt;Tracking all the television I watch has to count as one of the nerdier things I do. It also results in horrifying statistics like discovering that in 2023, &lt;a href=&quot;https://trakt.tv/users/spaceninja00/year/2023&quot;&gt;I watched 420 hours of television across 48 shows&lt;/a&gt;. But, you know, fuck it. I love good storytelling, and we’re living through a golden age of TV. Here’s some of the best stuff I watched this year:&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/extraordinary.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14531842/&quot;&gt;&lt;em&gt;Extraordinary&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This show has a really simple premise: What if everyone in the world got super powers except you? Building from that gives the actors here a lot of room to play. Jen, who has no powers and is really frustrated about it, lives with her best friend Carrie, who has the power to channel the dead, which she uses at a law firm to resolve boring legal estate disputes. Carrie’s boyfriend Kash has the ability to rewind time slightly and is obsessed with starting a vigilante group. Oh, and then there’s the stray cat they adopt and name Jizzlord, who turns out to be a grown man whose power is to turn into a cat, but he got stuck that way for years and lost his memory. It’s so stupid and funny and full of good music and great outfits and Annie and I loved every minute of it.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/poker-face.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14269590/&quot;&gt;&lt;em&gt;Poker Face&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;All I needed to hear was “Natasha Lyonne in a murder mystery of the week” and I was on-board. When I found out it was created by Ryan Johnson (yeah, the &lt;em&gt;Knives Out&lt;/em&gt; guy), I was even more on board.&lt;/p&gt;
&lt;p&gt;The essence of the show is that Lyonne’s character has the ability to tell when people are lying to her, and a complete inability to accept that. Her most common line is “bullshit” when someone lies to her, whether it’s appropriate or not. She’s on the run from the mob, so every episode involves her arriving in a new location and getting a shitty job that pays cash, and somehow getting wrapped up in a crime. She knows she should leave it alone, but she can’t just walk away. Seriously, it’s &lt;em&gt;so good&lt;/em&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/scott-pilgrim-takes-off.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt16969708/&quot;&gt;&lt;em&gt;Scott Pilgrim Takes Off&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You may have heard they made an anime remake of &lt;em&gt;Scott Pilgrim&lt;/em&gt; will all the actors from the movie coming back to voice their characters. That’s charming and cute, but wasn’t enough to make me want to watch it until I found this out: Episode one closely tracks the movie with a few cute changes, ending with the fight between Scott and Matthew Patel, the first evil ex. But in the anime, Patel wins, shocking everyone. From that point forward, Ramona is the main character, and it’s an excellent way to revisit these characters with a bit more maturity and insight.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/strange-new-worlds.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt12327578/&quot;&gt;&lt;em&gt;Star Trek: Strange New Worlds&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Years ago, my brother Sean and I were complaining about the lack of good Star Trek and Star Wars. These were worlds we loved, and wanted to spend more time in. Why were they just putting out one movie every few years? Let’s have more! Let’s have TV shows and multiple movies across multiple styles and genres! Where’s my Star Wars detective show? What about a Star Trek high school drama set at Starfleet Academy? Give me more!&lt;/p&gt;
&lt;p&gt;Well, the years have been kind to us and both Star Trek and Star Wars now have multiple shows and movies. Star Wars nailed the “more” part, but Star Trek nailed the “multiple styles” part. We have an embarrassment of riches now. &lt;em&gt;Picard&lt;/em&gt; is for the hardcore old-school fans, full of deep lore and references. &lt;em&gt;Discovery&lt;/em&gt; is the next main Trek show, following the bridge crew of a new ship having new adventures (especially starting in season three, when they made the excellent decision to throw the ship into the distant future, where they’re not so hamstrung by continuity). &lt;em&gt;Lower Decks&lt;/em&gt; is loving satire and homage to &lt;em&gt;The Next Generation&lt;/em&gt; era. &lt;em&gt;Prodigy&lt;/em&gt; is young adult animation and &lt;em&gt;Voyager&lt;/em&gt; homage.&lt;/p&gt;
&lt;p&gt;But &lt;em&gt;Strange New Worlds&lt;/em&gt; might be my favorite of them all. More than any other show has ever done, they’ve successfully threaded the needle of making a show with the &lt;em&gt;vibe&lt;/em&gt; of the original series, without the baggage. They gleefully diverge from canon when it makes sense, while also filling the show with little moments that will make long-time fans squeal.&lt;/p&gt;
&lt;p&gt;Season one features a Vulcan body-swap episode, romantic tension between Spock and nurse Chapel, and loads of exploration and delight at finding new things. They’ve got plenty of great callbacks to the original series (including some dedicated recreations of scenery, lighting, and music), while also updating everything to reflect changes in culture since the sixties.&lt;/p&gt;
&lt;p&gt;I seriously can’t recommend it enough. You can come in blind without knowing anything about Star Trek, or watch it as a long-time fan. It’s just genuinely great, and I hope they make a load of seasons.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/tv/andor.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt9253284/&quot;&gt;&lt;em&gt;Andor&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the same vein, &lt;em&gt;Andor&lt;/em&gt; manages to deliver a big-time story to the Star Wars universe. We originally met Cassian Andor as a hard-hearted spy in &lt;em&gt;Rogue One&lt;/em&gt;, and this series explores his backstory. Along the way, we get to see the changes happening across the galaxy as the Empire becomes more and more fascist, and how slow the population is to react. Andor himself is only grudgingly pulled into the resistance, and the story gives enough room to breathe and let us slowly explore how one man becomes radicalized. It’s dark at times, and refreshingly adult compared to the main films’ simplistic “good will triumph” vibe. Excellent TV, and exactly what Sean and I wanted more of — let people tell more stories in this world.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

    </content>
	</entry>
	<entry>
		<title>Movies I Loved in 2023</title>
		<link href="https://spaceninja.com/blog/2023/movies-i-loved-in-2023/"/>
		<published>2023-12-31T00:00:00Z</published>
		<updated>2023-12-31T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2023/movies-i-loved-in-2023/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="reviews" />
    <category term="movies" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/jawan-EvEuPy0ALP-1600w.jpeg&quot;&gt;
      &lt;p&gt;What a great year for movies. I had trouble whittling this list down to the best. &lt;a href=&quot;https://letterboxd.com/spaceninja/year/2023/&quot;&gt;I watched 150 movies this year&lt;/a&gt; and I’d say more than usual were fantastic. Here are some of the best.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/baby-assassins.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt15028452/&quot;&gt;&lt;em&gt;Baby Assassins&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Chisato and Mahilo are two high school girls who are about to graduate. They also happen to both be highly skilled assassins.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For reasons, the organization these girls work for forces them to become roommates and get jobs. While struggling to adapt to the real world, they piss off the Yakuza and are drawn into a whirlwind of violence. It’s a great female friendship movie, with a bit of an odd couple vibe, and a surprising amount of humor, especially when they both try to get jobs at a maid cafe. Ollie and I watched this together and had a great time.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/the-banshees-of-inisherin.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt11813216/&quot;&gt;&lt;em&gt;The Banshees of Inisherin&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Two lifelong friends find themselves at an impasse when one abruptly ends their relationship, with alarming consequences for both of them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It’s a great absurd premise: A man attempts to cut off all contact with his dull lifelong friend so he can focus on music, to the great confusion of his friend and the surrounding village. Colin Farrell and Brendan Gleeson are always a joy to watch, and Farrell’s constant frustration and confusion in the face of Gleeson’s steadfast rejection is perfect. Early on. Gleeson attempts to communicate how serious he is by threatening to cut off one of his own fingers every time Farrell talks to him. Mild spoiler warning: He is forced to follow through on this grim threat repeatedly. All told, it was an expertly crafted dark comedy, and I do recommend it, but be aware it gets a bit heavier than the trailer implies.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/el-conde.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt21113540/&quot;&gt;&lt;em&gt;El Conde&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;After living 250 years in this world, Augusto Pinochet, who is not dead but an aged vampire, decides to die once and for all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sometimes a movie comes with an elevator pitch that so perfectly absurd that you know you’re going to watch it. In this case, the dictator Augusto Pinochet is a 250-year old vampire, and is ready to die. I mean… come on. The film is at turns great and clumsy. It’s not perfect, but I have so much respect for a movie that swings for the fences like this. I greatly enjoyed it. There’s a bit of overlap with &lt;em&gt;Succession&lt;/em&gt;, in the sense that a powerful man is stepping down, and his family and entourage are circling, trying to figure out how to work the situation to their advantage. Anyways, it’s strange and unique, and worth your time.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/jawan.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt15354916/&quot;&gt;&lt;em&gt;Jawan&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A high-octane action thriller which outlines the emotional journey of a man who is set to rectify the wrongs in the society.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Oliver and I have started watching big-budget Bollywood movies together. I love the fight scenes and dance scenes and the moments of absurdity that come from cross-cultural entertainment. Ollie loves the constant gay subtext. Male characters (and especially Shak Rukh Khan) are extremely proper around female characters, even the supposed love interest, while being overtly intimate with male companions. No doubt this is just a culture clash thing, but every time SRK kisses a male friend, Ollie can’t contain themselves.&lt;/p&gt;
&lt;p&gt;This particular SRK movie is basically the plot of &lt;em&gt;Money Heist&lt;/em&gt;, but Bollywood. SRK is the leader of a gang of women who commit over-the-top heists aimed at exposing government corruption to the masses. This Robin Hood story has some rough moments that felt a bit forced, but overall landed well, and the mid-movie reveal that SRK is playing two characters was great, and let him get some moments of absurd comedy during the climactic confrontation.&lt;/p&gt;
&lt;p&gt;Also, there is a mafia boss who wears a Bane-style mask for no reason and is never explained or heard from again, and I love it.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/leave-the-world-behind.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt12747748/&quot;&gt;&lt;em&gt;Leave the World Behind&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A family&#39;s getaway to a luxurious rental home takes an ominous turn when a cyberattack knocks out their devices—and two strangers appear at their door.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A late entry delivered by Netflix, this is the best end of the world movie I’ve ever seen reflecting the character’s confusion in the absence of clear information. It’s arguably a horror film, but there are no jump scares here, only constantly ratcheting tension as a group of people stranded in a remote house try to figure out what the hell is going on. Julia Roberts has great fun playing against type as a fairly awful and moderately racist mother. Ethan Hawke plays a perfect confused and overwhelmed dad. Mahershala Ali’s restrained efforts to keep the white people from freaking out is balanced by his daughter Myha’la Herrold’s bluntness. I was surprised by how much I liked it, and I ended up watching it again a few nights later with my brother-in-law, and appreciated it even more the second time.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/prey.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt11866324/&quot;&gt;&lt;em&gt;Prey&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Naru, a skilled warrior of the Comanche Nation, fights to protect her tribe against one of the first highly-evolved Predators to land on Earth.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, if I told you that they made a prequel to &lt;em&gt;Predator&lt;/em&gt; involving an alien hunting native Americans you might be rightly suspicious that the film would be crap. But it’s not. It’s incredible, with great acting, phenomenal visuals, and a solid plot. They cast native actors, they don’t talk down to the audience, and deliver a great coming-of-age story that happens to involve hunting (and being hunted by) a terrifying monster from the sky. Ignore whatever you think about the Predator franchise and just make time to watch this as soon as possible.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/sisu.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt14846026/&quot;&gt;&lt;em&gt;Sisu&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;When an ex-soldier who discovers gold in the Lapland wilderness tries to take the loot into the city, Nazi soldiers led by a brutal SS officer battle him.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A simple John-Wick style “they crossed the wrong dude” movie, but done to absolute perfection. A group of Nazis are retreating through Lapland, and run across an old miner taking his gold into town. They attack him to steal the gold, and it goes extremely badly for them. The trailer features a scene where the Nazis have driven the old man into the fog in a minefield. The Nazi commander orders one of his soldiers forward when a landmine comes flying through the air, hitting the soldier’s helmet and exploding him. At one point, the old man lights himself on fire for seemingly no reason other than to terrify the Nazis. Highly recommended.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/movies/they-cloned-tyrone.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.imdb.com/title/tt9873892/&quot;&gt;&lt;em&gt;They Cloned Tyrone&lt;/em&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A series of eerie events thrusts an unlikely trio onto the trail of a nefarious government conspiracy lurking directly beneath their neighborhood.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;John Boyega is a drug dealer in a poor neighborhood called the Glen. Jamie Foxx sees him shot to death one night, and is shocked when he shows up alive the next day. Together with Teyonah Parris, they uncover a bizarre government conspiracy. I don’t want to say much else for fear of spoiling it, but just trust me, it’s a barrel ride of a film, and you should watch it.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

    </content>
	</entry>
	<entry>
		<title>Books I Loved in 2023</title>
		<link href="https://spaceninja.com/blog/2023/books-i-loved-in-2023/"/>
		<published>2023-12-30T00:00:00Z</published>
		<updated>2023-12-30T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2023/books-i-loved-in-2023/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="reviews" />
    <category term="books" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/books-2023-HnGcatdKbs-1600w.jpeg&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.goodreads.com/user_challenges/40607145&quot;&gt;I read 25 books in 2023&lt;/a&gt;. My goal was 24, which I’ve hit every year for a few years now, but I have a secret: A good chunk of the books I was reading were actually books I was reading to my son John. We read &lt;em&gt;Lord of the Rings&lt;/em&gt;, some &lt;em&gt;Harry Potter&lt;/em&gt;, some &lt;em&gt;Hitchhiker’s Guide&lt;/em&gt;, and some &lt;em&gt;Discworld&lt;/em&gt; books. But about a year ago, he kinda grew out of this phase and didn’t want me to read to him at bedtime anymore. Which is fine, I love that he let me for so long and that I was able to pass along a love of reading.&lt;/p&gt;
&lt;p&gt;But it did mean that I lost a pile of books that I would have read otherwise, and I had less reading time overall, because I wasn’t sitting in his room reading my own books while he fell asleep. So the fact that I still hit my goal feels good, but it does tell me that in 2024, I need to be more intentional about carving out time to read.&lt;/p&gt;
&lt;p&gt;Until then, here are the best books I read in 2023.&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/last-one-at-the-party.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/60763072-last-one-at-the-party&quot;&gt;&lt;em&gt;Last One at the Party&lt;/em&gt;&lt;/a&gt;, by Bethany Clift&lt;/h2&gt;
&lt;p&gt;The basic premise is “What if the person who survived the end of the world was uniquely unsuited for post-apocalyptic life?”&lt;/p&gt;
&lt;p&gt;Presented as a series of journal entries documenting how she deals with the complete collapse of the world (hint: poorly, then VERY poorly, then less poorly). It’s funny and charming at times, depressing and overwhelming at others, and generally does a good job of feeling like a realistic look at how someone with no survival skills would handle things.&lt;/p&gt;
&lt;p&gt;I liked this so much that I recommended it to my wife, which happens infrequently enough to be noteworthy.&lt;/p&gt;
&lt;small&gt;
&lt;p&gt;&lt;em&gt;Content warnings: This book features a dog and a pregnancy, and I’ve added &lt;a href=&quot;https://www.doesthedogdie.com/media/1037847&quot;&gt;an entry on DoesTheDogDie.com&lt;/a&gt; if you&#39;d find that helpful.&lt;/em&gt;&lt;/p&gt;
&lt;/small&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/half-built-garden.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/41637112-a-half-built-garden&quot;&gt;&lt;em&gt;A Half-Built Garden&lt;/em&gt;&lt;/a&gt;, by Ruthanna Emrys&lt;/h2&gt;
&lt;p&gt;A fascinating first-contact story about aliens landing in the Chesapeake Bay, where they are met by Judy, a woman from an ecological commune called a watershed network. The aliens are here to encourage (or force) humanity to flee the planet, but Judy isn’t ready to abandon the restoration work they’ve been doing — or allow the exiled remnants of corporations and nation-states to make the decision for everyone else.&lt;/p&gt;
&lt;p&gt;It’s the best kind of hopeful queer science fiction. I loved every part of this story, and I bet you will too.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/expanse9.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/57397125-leviathan-falls&quot;&gt;&lt;em&gt;Leviathan Falls&lt;/em&gt;&lt;/a&gt;, by James S.A. Corey&lt;/h2&gt;
&lt;p&gt;I’ve told you before how much &lt;a href=&quot;https://spaceninja.com/blog/2018/books-i-love-the-expanse-series/&quot;&gt;I love the Expanse series&lt;/a&gt;, but I put off reading the final books in the series for some time. The TV series caught up to me when I finished book five, and I opted to watch season six without reading the book to avoid spoilers.&lt;/p&gt;
&lt;p&gt;So now that the show is over (or at least on hiatus for a time?), I came back to the books. Book six was a quick read, covering the same events as the show (though the shortened final season had to cut quite a lot of stuff). But book seven jumps several years into the future, following the crew of the Rocinante as they deal with the complex reality after the opening of the gate network.&lt;/p&gt;
&lt;p&gt;I can’t really say much about it without spoiling anything, so I’ll just say that I thought this was a solid ending to the story. The final scenes with each character felt genuine and earned. If you’ve watched the show or read the books you know enough to not expect a happy-ever-after, but everyone ends up somewhere satisfyingly true to their characters.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;img src=&quot;https://spaceninja.com/_assets/_images/books/stolen-focus.jpg&quot; alt=&quot;&quot; sizes=&quot;(min-width: 48em) 250px, (min-width: 40em) 200px, (min-width: 32em) 150px, 100px&quot; eleventy:widths=&quot;300,400,500&quot;&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.goodreads.com/book/show/57933306-stolen-focus&quot;&gt;&lt;em&gt;Stolen Focus: Why You Can’t Pay Attention—and How to Think Deeply Again&lt;/em&gt;&lt;/a&gt;, by Johann Hari&lt;/h2&gt;
&lt;p&gt;I’ll be honest: The first two times Chuck recommended this book to me, I kinda shrugged him off. It sounded like a self-help book, where the author would dive into a lot of stuff I already know about how social network algorithms are designed to keep us clicking, and ending with some trite advice that boils down to “take Twitter off your phone” and “have more self-control.”&lt;/p&gt;
&lt;p&gt;But the thing about Chuck is that he’s got ADHD so sometimes he’ll recommend something to me repeatedly by mistake. In this case, in three different conversations, weeks apart, something I said about burnout or focus triggered him to recommend it. And by the third time, I realized I needed to check it out.&lt;/p&gt;
&lt;p&gt;That said, I very nearly rage-quit this book. The first few chapters are some of the most insufferable shit I’ve ever read. He shares an infuriating anecdote about harassing visitors to Graceland and then snatching the phone out of his teenage nephew’s hand and yelling at him in public, all with a smug “am I the only one who sees what’s happening” vibe. After that, he talks in the most eye-rolling way about the difficulties he faced during a three-month digital detox while staying in a cabin on the beach.&lt;/p&gt;
&lt;p&gt;But one night as I was angrily complaining about the book to Annie, she asked if there was anything valuable, and I was forced to concede that the chapter I’d just read had an interesting bit. I asked her to pass me a highlighter, and I marked it down so I wouldn’t lose it. Then I went back to the chapters I’d already finished and I found at least one or two valuable passages to highlight in each. So I resolved to keep reading, highlighting the worthwhile bits as I went.&lt;/p&gt;
&lt;p&gt;And I’m glad I did, because the book makes a pretty remarkable shift about halfway through, moving away from “here’s what I learned on my digital detox” with an emphasis on individual actions into a strongly condemning discussion about the systemic problems that we all face that mean individual actions are unlikely to address attention problems for most people.&lt;/p&gt;
&lt;p&gt;From that point, the book is really about the author getting slowly radicalized by how our attention has been stolen from us, and discussing the broad changes that we need to make, from outlawing surveillance capitalism to addressing nutrition, to the way that children are no longer allowed to play or have unstructured time.&lt;/p&gt;
&lt;p&gt;I thought this was a self-help book, but it’s not. It’s a call to action, and I think everyone should read it.&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;

    </content>
	</entry>
	<entry>
		<title>Friday Front-End’s Top Links of 2023</title>
		<link href="https://spaceninja.com/blog/2023/ff-top-ten-2023/"/>
		<published>2023-12-15T00:00:00Z</published>
		<updated>2023-12-15T00:00:00Z</updated>
		<id>https://spaceninja.com/blog/2023/ff-top-ten-2023/</id>
    <author>
      <name>Scott Vandehey</name>
    </author>
    <category term="blog" />
    <category term="frontend" />
    <category term="javascript" />
    <category term="css" />
		<content type="html">
      &lt;img alt=&quot;&quot; src=&quot;https://spaceninja.com/images/feature-EWj6KDf4cH-1600w.jpeg&quot;&gt;
      &lt;p&gt;In 2023, &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Friday Front-End&lt;/a&gt; shared a curated list of five articles and one video every week. Here are the links that were most popular:&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.leereamsnyder.com/web-component-and-somehow-also-js-101&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/web-component-and-somehow-also-js-101.jpeg&quot; alt=&quot;Messin’ around with web components. Also—JavaScript, generally&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;10: &lt;a href=&quot;https://www.leereamsnyder.com/web-component-and-somehow-also-js-101&quot;&gt;Messin’ around with web components. Also—JavaScript, generally&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;I think I have to use the constructor() here since I’m setting this. But also there are no good blog posts out there explaining any of this stuff and so I challenge you, nay dare you, to really explain all this to me.&amp;quot;&lt;/p&gt;
&lt;p&gt;Ha, challenge accepted.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://chriscoyier.net/2023/11/27/the-hanging-punctuation-property-in-css/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/hanging-punctuation.jpeg&quot; alt=&quot;The `hanging-punctuation` property in CSS&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;9: &lt;a href=&quot;https://chriscoyier.net/2023/11/27/the-hanging-punctuation-property-in-css/&quot;&gt;The &lt;code&gt;hanging-punctuation&lt;/code&gt; property in CSS&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;hanging-punctuation&lt;/code&gt; property in CSS is almost a no-brainer. The classic example is a blockquote that starts with a curly-quote. Hanging that opening curly-quote into the space off to the start of the text and aligning the actual words is a better look.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://tylergaw.com/blog/complex-mpa-view-transitions/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/complex-mpa-view-transitions.jpeg&quot; alt=&quot;Complex MPA View Transitions&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;8: &lt;a href=&quot;https://tylergaw.com/blog/complex-mpa-view-transitions/&quot;&gt;Complex MPA View Transitions&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Full page transitions are OK, but they also make a site feel like a PowerPoint. The only time I’m likely to use full page transitions is if I’m making a web-based slide deck. Even then, using only full page animations can feel flat. Animating multiple elements in an orchestrated way is a better way to use motion to create interesting effects. So, that’s what I’ve done. Let’s get into it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://heather-buchel.com/blog/2023/10/why-your-web-design-sucks/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/here-is-why-your-design-sucks.png&quot; alt=&quot;It’s 2023, here is why your web design sucks.&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;7: &lt;a href=&quot;https://heather-buchel.com/blog/2023/10/why-your-web-design-sucks/&quot;&gt;It’s 2023, here is why your web design sucks.&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Exploring the reasons why we no longer have web designers. TLDR: At some point, we told design they couldn&#39;t sit with us anymore, and surprise! It backfired! Now, not only has the field and profession of web design suffered, but also, we build shitty websites.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://adactio.com/journal/20618&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/html-web-components-adactio.png&quot; alt=&quot;HTML Web Components&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;6: &lt;a href=&quot;https://adactio.com/journal/20618&quot;&gt;HTML Web Components&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;When you wrap some existing markup in a custom element and then apply some new behaviour with JavaScript, technically you’re not doing anything you couldn’t have done before with some DOM traversal and event handling. But it’s less fragile to do it with a web component. It’s portable. It obeys the single responsibility principle. It only does one thing but it does it well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://utilitybend.com/blog/elevate-your-css-debugging-skills-with-these-chrome-devtools-tricks-in-2024/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/chrome-devtools.webp&quot; alt=&quot;Elevate your CSS debugging skills with these Chrome DevTools tricks in 2024&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;5: &lt;a href=&quot;https://utilitybend.com/blog/elevate-your-css-debugging-skills-with-these-chrome-devtools-tricks-in-2024/&quot;&gt;Elevate your CSS debugging skills with these Chrome DevTools tricks in 2024&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Elevate your CSS debugging skills with these powerful Chrome DevTools tricks. Learn how to tackle layers, specificity, nesting, HD color, and scroll animations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://cloudfour.com/thinks/html-web-components-are-having-a-moment/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/html-web-components.png&quot; alt=&quot;HTML Web Components Are Having a Moment&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;4: &lt;a href=&quot;https://cloudfour.com/thinks/html-web-components-are-having-a-moment/&quot;&gt;HTML Web Components Are Having a Moment&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Eric’s post showed me that web components could be simpler than I thought. Jeremy came up with a brilliantly approachable name with “HTML web components.” And everyone else who wrote about them in the following weeks added to the growing sense that, yes, this was a thing that I could do, that had clear value, and I began looking for opportunities to use them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://poke-holo.simey.me/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/pokemon.png&quot; alt=&quot;Pokémon Cards CSS Holographic Effect&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;3: &lt;a href=&quot;https://poke-holo.simey.me/&quot;&gt;Pokémon Cards CSS Holographic Effect&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;A collection of advanced CSS styles to create realistic-looking effects for the faces of Pokemon cards. The cards use 3d transforms, filters, blend modes, css gradients and interactions to provide a unique experience when taking a closer look!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://css-tricks.com/solved-with-has-vertical-spacing-in-long-form-text/&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/solved-with-has.jpeg&quot; alt=&quot;Solved With :has(): Vertical Spacing in Long-Form Text&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;2: &lt;a href=&quot;https://css-tricks.com/solved-with-has-vertical-spacing-in-long-form-text/&quot;&gt;Solved With :has(): Vertical Spacing in Long-Form Text&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;To recap the improvements this aims to make: No wrapper class is required; We’re working with a consistent margin direction; Collapsing margins are avoided (which may or may not be an improvement, depending on your stance); There’s no setting styles and then immediately overriding them;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://dev.to/francescovetere/the-css-property-you-didnt-know-you-needed-3fk0&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/the-css-property-you-didnt-know-you-needed.png&quot; alt=&quot;The CSS property you didn’t know you needed&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;1: &lt;a href=&quot;https://dev.to/francescovetere/the-css-property-you-didnt-know-you-needed-3fk0&quot;&gt;The CSS property you didn’t know you needed&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Today I wanna talk about a CSS feature that doesn&#39;t get too much attention… but it should! The &lt;code&gt;isolation&lt;/code&gt; property basically provides more control over how elements interact with the rest of the document, and it is often an elegant solution for &lt;code&gt;z-index&lt;/code&gt; issues.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here&#39;s the most popular video of 2023:&lt;/p&gt;
&lt;ul class=&quot;media-list&quot;&gt;
&lt;li class=&quot;media-list__item&quot;&gt;
&lt;div class=&quot;media-list__media&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=_2LwjfYc1x8&quot;&gt;&lt;img src=&quot;https://spaceninja.com/_assets/_images/ff-top-ten-2023/custom-properties.jpeg&quot; alt=&quot;Using CSS custom properties like this is a waste&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;media-list__content&quot;&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=_2LwjfYc1x8&quot;&gt;Using CSS custom properties like this is a waste&lt;/a&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Custom properties are amazing, but a lot of people don’t take advantage of how awesome they are. They set them up in the &lt;code&gt;:root&lt;/code&gt; and that’s it, but they can be so much more useful than that! So, in this video I explore how we take them up a notch and make our code a lot more efficient in the process.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Want to enjoy more great links like this in 2024? &lt;a href=&quot;https://fridayfrontend.com/&quot;&gt;Subscribe to the Friday Front-End newsletter&lt;/a&gt;, or &lt;a href=&quot;https://hachyderm.io/@fridayfrontend&quot;&gt;follow @fridayfrontend on Mastodon&lt;/a&gt;!&lt;/p&gt;

    </content>
	</entry>
</feed>
