<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Jonathan Yeong</title><description>Hey there, I&apos;m Jono Yeong 👋. 				I&apos;m a Staff Software Engineer from Australia currently based in Canada. 				This blog is a collection of notes on topics that I find interesting and exciting.</description><link>https://jonathanyeong.com/</link><item><title>npmx and the open source mindset</title><link>https://jonathanyeong.com/writing/npmx-and-the-open-source-mindset/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/npmx-and-the-open-source-mindset/</guid><description>Thoughts on my journey contributing to npmx over the past month. The future of open source is bright.</description><pubDate>Tue, 03 Mar 2026 22:17:05 GMT</pubDate><content:encoded>&lt;p&gt;The beauty of open source is that no one is forced to be there. This is what hit me when I first started contributing to &lt;a href=&quot;https://npmx.dev/?ref=newsletter.jonathanyeong.com&quot;&gt;npmx&lt;/a&gt;. I joined the project after seeing a call to action from &lt;a href=&quot;https://bsky.app/profile/patak.cat/post/3mdaxxvdnv22s?ref=newsletter.jonathanyeong.com&quot;&gt;patak&lt;/a&gt;. I don&apos;t particularly know why I joined. I think my fomo got the better of me. &lt;/p&gt;&lt;p&gt;When I entered the Discord, I felt a bit lost. I&apos;m a Ruby, Go, backend developer. What could I contribute to a Nuxt project? And one that builds on top of npm, a package registry I browse only on occasion. Where do I even begin?&lt;/p&gt;&lt;p&gt;So I did what I usually do. I combed through &quot;good first issues&quot; looking for unassigned tickets. But looking around, nobody was asking permission to pick up an issue. Nobody was concerned about multiple people working on the same thing. &lt;/p&gt;&lt;p&gt;Seeing how the community acted empowered me to do the same. I picked up a bug, dug into the code, and pushed a PR. Immediately people jumped in and started commenting. One person was working on something similar. Another had a suggested change to the PR. All of the discussion made my PR better. And every interaction felt like they cared.&lt;/p&gt;&lt;p&gt;This PR along with the Discord interactions blew my corporate mind. In a corporate environment, I would&apos;ve asked whether this ticket was estimated, or prioritized. I would&apos;ve asked about aligning the approach before making the PR. I definitely felt like I missed a step having multiple people commenting on the approach of the PR. But this is open source. People do these things because they care.&lt;/p&gt;&lt;p&gt;That&apos;s the open source mindset. You decide what to work on. You decide when. Nobody is keeping score, nobody is checking in, nobody is assigning you work. The only thing driving you is wanting to make the project better. &lt;/p&gt;&lt;p&gt;It&apos;s a mindset I could only adopt by being a part of this community. &lt;a href=&quot;https://bsky.app/profile/patak.cat?ref=newsletter.jonathanyeong.com&quot;&gt;Matias (aka patak)&lt;/a&gt;, &lt;a href=&quot;https://bsky.app/profile/danielroe.dev?ref=newsletter.jonathanyeong.com&quot;&gt;Daniel Roe&lt;/a&gt;, &lt;a href=&quot;https://bsky.app/profile/whitep4nth3r.com?ref=newsletter.jonathanyeong.com&quot;&gt;Salma&lt;/a&gt;, and many others created a place of belonging. They showed up every day, in every channel offering suggestions and encouragement. And they put this culture into writing in a &lt;a href=&quot;https://github.com/npmx-dev/npmx.dev/blob/main/GOVERNANCE.md?ref=newsletter.jonathanyeong.com&quot;&gt;Governance doc&lt;/a&gt;:&lt;/p&gt;&lt;blockquote&gt;Anyone can become an npmx contributor. Engineering ability is not required &lt;/blockquote&gt;&lt;p&gt;The community is the secret sauce. Something that AI can never replace. The fact that places of belonging and action outside of corporations exist makes me optimistic about the future of open source. I&apos;m incredibly grateful to have been a part of this journey. There&apos;s much more to come, &lt;a href=&quot;https://npmx.dev/blog/alpha-release?ref=newsletter.jonathanyeong.com&quot;&gt;since npmx is now in alpha.&lt;/a&gt; If you&apos;ve been wanting to contribute to open source and are looking for a welcoming community, come &lt;a href=&quot;https://build.npmx.dev/?ref=newsletter.jonathanyeong.com&quot;&gt;join us&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Using Val.town as a webhook middleware</title><link>https://jonathanyeong.com/writing/using-val-town-as-a-webhook-middleware/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/using-val-town-as-a-webhook-middleware/</guid><description>My latest obsession has been optimising webhook events from Ghost to Netlify. I needed a way to filter out large payloads sent to Netlify. Using Val.town as a webhook middleware, I can strip payloads and skip triggering builds for certain posts.</description><pubDate>Tue, 13 Jan 2026 22:06:19 GMT</pubDate><content:encoded>&lt;p&gt;Productive procrastination is one of my favourite pastimes. My latest obsession has been webhook events from &lt;a href=&quot;https://ghost.org/?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost CMS&lt;/a&gt; to Netlify. I had event listeners in Ghost that triggered builds whenever the site changed. When this event occurred, and it occurred often, it would trigger a Netlify build by POSTing to a &lt;a href=&quot;https://docs.netlify.com/build/configure-builds/build-hooks/?ref=newsletter.jonathanyeong.com#payload&quot;&gt;Netlify build hook&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;I wanted to optimise these webhook events to run fewer builds on Netlify. This meant using fewer build minutes, which &lt;em&gt;may&lt;/em&gt; translate to&lt;em&gt; &lt;/em&gt;less money spent. Much productive. You might ask, &quot;how many build minutes are you using?&quot;. Well my friend, I&apos;m not even close to the free build minute quota. Nevertheless, we press on.&lt;/p&gt;&lt;p&gt;The first thing I did was listen to different events that fired less often. Instead of site changes, I now listen to post published, updated, and unpublished events. This sends a payload that looks something like this to Netlify:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;695d72563020fd0001cb96d6&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;uuid&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;41af1fc2-40e9-43e8-aa47-fbe4de2fd276&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;Test Newsletter post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;test-newsletter-post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;&amp;lt;h1&amp;gt;Some HTML&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          ...some fields&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// ...Many other fields&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Unfortunately, if the blog post is long it will cause a Netlify build error. I don&apos;t see it documented, but I think there&apos;s a &lt;a href=&quot;https://answers.netlify.com/t/build-payload-exceeded-limit-of-140-kb-hugo-and-sanity-io/88812?ref=newsletter.jonathanyeong.com&quot;&gt;140kb limit&lt;/a&gt; on payloads sent to Netlify build hooks. I needed a way to process this payload &lt;em&gt;before&lt;/em&gt; sending it to Netlify. Enter &lt;a href=&quot;https://www.val.town/dashboard?ref=newsletter.jonathanyeong.com&quot;&gt;Val.town&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;In Val.town, you create building blocks called &lt;a href=&quot;https://docs.val.town/vals/?ref=newsletter.jonathanyeong.com&quot;&gt;Vals&lt;/a&gt; that are used to run little http apps, crons, or email handlers all with Javascript/Typescript. I first heard of Val.town from Sam Rose who uses it to power the &lt;a href=&quot;https://samwho.dev/ping?ref=newsletter.jonathanyeong.com&quot;&gt;ping functionality on his website&lt;/a&gt; (see &lt;a href=&quot;https://www.val.town/x/samwho/notify/branches?ref=newsletter.jonathanyeong.com&quot;&gt;his notify Val&lt;/a&gt;). I can use a Val as an intermediate service that will do some processing on a webhook payload before sending it off to Netlify. &lt;/p&gt;&lt;p&gt;Here&apos;s the code in its entirety. Let&apos;s break it down.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; default&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; (req&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Request&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;Response&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;method&lt;/span&gt;&lt;span&gt; !==&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Method Not Allowed&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 405&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; payload&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Ghost CMS Webhook Payload:&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;(payload,&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; payload&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post?.current;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    // Don&apos;t continue if the post has hash-newsletter&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; hasNewsletterTag&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; post?.tags?.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;((tag&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      tag&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;span&gt; ===&lt;/span&gt;&lt;span&gt; &quot;hash-newsletter&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (hasNewsletterTag)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Post is meant to be a newsletter, skipping&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Post has hash-newsletter tag&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 200&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&amp;lt;netlify-build-hook-url&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        headers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          &quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        body&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({}),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ok)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Failed to trigger Netlify build&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;span&gt; catch&lt;/span&gt;&lt;span&gt; (error)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Something went wrong&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 500&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;OK&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 200&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Breaking down webhook Val&lt;/h2&gt;&lt;p&gt;First off, I created a &lt;a href=&quot;https://docs.val.town/vals/http/?ref=newsletter.jonathanyeong.com&quot;&gt;Val with a HTTP trigger&lt;/a&gt;. This gives me a URL that I can use to trigger the Val from Ghost. It looks like:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;https://jonoyeong--50a8ba8ef0c611f0a43942dde27851f2.web.val.run&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next up is the guard clause. When you open a Val it will immediately run a GET request to the above URL. This results in an error since the request is not in the format we expect. The guard clause below returns a nicer response and is easier to parse in the logs. &lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;method &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Method Not Allowed&quot;&lt;/span&gt;&lt;span&gt;, { status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 405&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we need to process the Ghost payload. We parse the payload data and grab the current post object.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; payload &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; req&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; payload&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post?.current&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Remember, the payload from Ghost is structured below.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &quot;&lt;/span&gt;&lt;span&gt;current&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;id&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;695d72563020fd0001cb96d6&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;uuid&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;41af1fc2-40e9-43e8-aa47-fbe4de2fd276&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;Test Newsletter post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;test-newsletter-post&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;&amp;lt;h1&amp;gt;Some HTML&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;&lt;/span&gt;&lt;span&gt;tags&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          ...some fields&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      ]&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// ...Many other fields&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you might wonder why we even need that data. Well we can do some more pre-processing here. I want Ghost to be the source of my blog and newsletter content. Anything that&apos;s tagged with &lt;code&gt;#newsletter&lt;/code&gt; shouldn&apos;t be pulled into my blog. The code below will check if any posts have a &lt;code&gt;#newsletter&lt;/code&gt; tag, represented as &lt;code&gt;hash-newsletter&lt;/code&gt; in the payload. And if it does, it will skip triggering a Netlify build.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// Don&apos;t continue if the post has hash-newsletter&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; hasNewsletterTag &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; post?.tags?.&lt;/span&gt;&lt;span&gt;some&lt;/span&gt;&lt;span&gt;((tag&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; any&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  tag&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slug &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &quot;hash-newsletter&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (hasNewsletterTag) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;log&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Post is meant to be a newsletter, skipping&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Post has hash-newsletter tag&quot;&lt;/span&gt;&lt;span&gt;, { status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 200&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, we call the Netlify build hook.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &quot;&amp;lt;netlify-build-hook-url&amp;gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    headers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    body&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({}),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;Failed to trigger Netlify build&quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I POST to Netlify, with an empty body to avoid any payload size errors. And in case something errors, I return a &lt;code&gt;Response&lt;/code&gt; with a message and status that will show up in my Val logs.&lt;/p&gt;&lt;p&gt;I found Val.town pretty nifty for deploying small services that manipulate data from 3rd party services you don&apos;t have full control over. There&apos;s other ways we can achieve build filtering as well. &lt;a href=&quot;https://melanie-richards.com/blog/webhook-payloads/?ref=newsletter.jonathanyeong.com&quot;&gt;Melanie Richards documented a couple of other methods&lt;/a&gt;. Funnily enough, I found out recently that &lt;a href=&quot;https://astro.build/blog/year-in-review-2025/?ref=newsletter.jonathanyeong.com#new-and-updated-features&quot;&gt;Astro is releasing a live content collection feature.&lt;/a&gt; Live content collections will fetch content on-demand, which might make all of the work above obsolete. I&apos;m not mad though. It just means I&apos;ll have another reason to productively procrastinate in the future. &lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Year in Review: 2025</title><link>https://jonathanyeong.com/writing/year-in-review-2025/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/year-in-review-2025/</guid><description>My 2025 year in review. This was a year of peaks and valleys. I touch on what I&apos;ve accomplished, my favourite lists this year, and my theme for next year.</description><pubDate>Wed, 31 Dec 2025 19:44:14 GMT</pubDate><content:encoded>&lt;p&gt;&lt;br /&gt;This was a year of peaks and valleys.&lt;/p&gt;&lt;p&gt;I started strong with a website redesign and a &lt;a href=&quot;https://jonathanyeong.com/writing/blog-redesign-v3/?ref=newsletter.jonathanyeong.com&quot;&gt;blog post recapping the work&lt;/a&gt;. It became my most viewed blog post of the year! I put a lot of effort into it, and I’m chuffed with how well it was received. Following the redesign, I started a newsletter which evolved from writing about the whims of my day to something focused on coding, writing, and design. &lt;/p&gt;&lt;p&gt;Then summer rolled around and I took an unplanned hiatus from being productive. It&apos;s the same old story, life and work got in the way. The very same inertia I felt pulling me up the peak, was now keeping me at rest. I stopped sending out newsletters. Stopped working on side projects. And blog posts sat in my drafts folder collecting digital dust.&lt;/p&gt;&lt;p&gt;Thankfully, I ended the year strong by publishing said posts. I&apos;m particularly proud of my &lt;a href=&quot;https://jonathanyeong.com/writing/mobile-friendly-headless-cmss-with-astro/?ref=newsletter.jonathanyeong.com&quot;&gt;post comparing different mobile friendly CMSs.&lt;/a&gt; I pushed myself to connect and contribute to digital communities again. Posting on Bluesky and joining the &lt;a href=&quot;https://discord.gg/d9gZyYuqKd?ref=newsletter.jonathanyeong.com&quot;&gt;Overcommitted Discord&lt;/a&gt;. I read the hundreds of unread articles in my RSS reader, and got back to tinkering with side projects. Essentially, the last bit of the year was focused on building the inertia to create again. &lt;/p&gt;&lt;p&gt;I&apos;m annoyed that I wasn&apos;t more consistent in 2025. But I did learn something about myself.&lt;/p&gt;&lt;blockquote&gt;Even if I stop, I&apos;m still able to come back.&lt;/blockquote&gt;&lt;p&gt;I&apos;ve always wondered why I work on side projects or write. Am I doing it for myself? Or because it&apos;s what others tell me I should do? But now I know I do it because I really enjoy it. Even if I take a long break, it&apos;ll always be there when I&apos;m ready. And there&apos;s something quite forgiving about this thought. &lt;/p&gt;&lt;h2&gt;This year in lists&lt;/h2&gt;&lt;h3&gt;Stuff I wrote&lt;/h3&gt;&lt;p&gt;I&apos;m very happy with the four blog posts I published. Even if it&apos;s less than last year.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/writing/blog-redesign-v3/?ref=newsletter.jonathanyeong.com&quot;&gt;Redesigning my site: Version 3&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/writing/mobile-friendly-headless-cmss-with-astro/?ref=newsletter.jonathanyeong.com&quot;&gt;Mobile-Friendly Headless CMS&apos;s with Astro&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/writing/loading-ghost-cms-posts-with-astro-content-collections/?ref=newsletter.jonathanyeong.com&quot;&gt;Loading Ghost CMS posts with Astro Content Collections&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/writing/lessons-i-learnt-from-my-first-year-of-building-a-devex-team/?ref=newsletter.jonathanyeong.com&quot;&gt;Lessons I learnt from my first year of building a DevEx team&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;I sent 15 newsletters. My fav was about &lt;a href=&quot;https://newsletter.jonathanyeong.com/developing-my-writing-style/&quot;&gt;developing my writing style&lt;/a&gt;. Surprisingly I actually achieved my newsletter goal which was to send 12 newsletters.&lt;/p&gt;&lt;h3&gt;Fav podcasts&lt;/h3&gt;&lt;p&gt;I listened to 9,187 minutes of podcasts this year. And these were my fav podcasts ranked by listen time!&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://overcommitted.dev/?ref=newsletter.jonathanyeong.com&quot;&gt;Overcommitted&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Fun podcast, with interesting topics and guests. I totally resonate with the developer that&apos;s overcommitted as well!&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://syntax.fm/?ref=newsletter.jonathanyeong.com&quot;&gt;Syntax&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;My go to podcast to keep up to date with web development.&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://growthinreverse.com/podcast/?ref=newsletter.jonathanyeong.com&quot;&gt;Growth in reverse&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;A podcast about newsletter growth strategies. Even though I haven&apos;t sent a newsletter in a long time, it&apos;s interesting hearing about how newsletters operate behind the scenes.&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://www.fafo.fm/?ref=newsletter.jonathanyeong.com&quot;&gt;Fork around and find out&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;Great tech guests and the main infra focused podcast I listen to.&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://shoptalkshow.com/?ref=newsletter.jonathanyeong.com&quot;&gt;ShopTalk Show&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;I came to learn about web dev. But I stayed for the top notch banter.&lt;/p&gt;&lt;h3&gt;Fav games&lt;/h3&gt;&lt;h4&gt;&lt;a href=&quot;https://store.steampowered.com/app/892970/Valheim/?ref=newsletter.jonathanyeong.com&quot;&gt;Valheim&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;It&apos;s a nordic open world survival game with deep building and combat mechanics. You play as a viking reborn in Valhalla, either solo or co-op. It&apos;s very fun and very addicting with friends. I felt like a viking, travelling the vast ocean to explore new lands.&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://store.steampowered.com/app/1145350/Hades_II/?ref=newsletter.jonathanyeong.com&quot;&gt;Hades 2&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;A rogue like dungeon crawler with beautiful graphics, sound design, and an addictive gameplay loop. The dialog with Gods of Olympus alone is what makes me want to start another run.&lt;/p&gt;&lt;h4&gt;&lt;a href=&quot;https://store.steampowered.com/app/2239150/Thronefall/?ref=newsletter.jonathanyeong.com&quot;&gt;Thronefall&lt;/a&gt;&lt;/h4&gt;&lt;p&gt;A cute tower defense game. I got it recently because I wanted something that was more chill than Valheim and Hades 2. It&apos;s perfect if you only have 30 minutes and you want something strategic but low stakes.&lt;/p&gt;&lt;h2&gt;Future plans&lt;/h2&gt;&lt;p&gt;Next year, I just want to be able to answer this question:&lt;/p&gt;&lt;blockquote&gt;What have you been up to since I&apos;ve seen you last? &lt;/blockquote&gt;&lt;p&gt;I don&apos;t know why, but when someone asks me this question, my mind goes into a panic. What &lt;em&gt;have&lt;/em&gt; I been doing? Why can&apos;t I remember? Is living an answer?&lt;/p&gt;&lt;p&gt;I&apos;ll be theming my year around &lt;strong&gt;intentionality&lt;/strong&gt;. And by doing so I&apos;m hoping to be more aware and present in what I do. To be more intentional with my time and to remember what the hell I&apos;ve been doing.&lt;/p&gt;&lt;p&gt;I&apos;m grateful for all the connections I&apos;ve made in 2025. It&apos;s been an up and down year and I appreciate all of you who have stayed in touch even after I dropped off the grid for half the year. Sending you all good vibes for 2026. &lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Lessons I learnt from my first year of building a DevEx Team</title><link>https://jonathanyeong.com/writing/lessons-i-learnt-from-my-first-year-of-building-a-devex-team/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/lessons-i-learnt-from-my-first-year-of-building-a-devex-team/</guid><description>With seemingly every company adopting AI, the need for good DevEx is greater than ever. I&apos;ve spent all of 2025 building a DevEx team and I have some thoughts around the need for DevEx, ownership, product thinking, and hiring.</description><pubDate>Wed, 03 Dec 2025 23:58:21 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve been running a DevEx team all of 2025, and I really wished I had some sort of guidebook when I started. And then last week, Nicole Forsgren and Abi Noda published &lt;a href=&quot;https://developerexperiencebook.com/?ref=newsletter.jonathanyeong.com&quot;&gt;Frictionless&lt;/a&gt;. A handbook for starting a DevEx team. Where were you at the start of the year?! At least I can compare notes. Here&apos;s my attempt at trying to process everything I learnt about DevEx this year.&lt;/p&gt;&lt;h2&gt;The need for DevEx is super clear&lt;/h2&gt;&lt;p&gt;Starting the year I knew DevEx was important. I don&apos;t think I realised how important. When I started the team, our goal was to reduce developer friction in their day to day workflow. The theory was reducing friction equals happy devs. And happy devs are less burnt out, more satisfied in their role, innovate faster and deliver more. &lt;/p&gt;&lt;p&gt;Now that my company, and seemingly every tech company, has adopted AI the need for good DevEx is even greater. With AI tooling, devs are able to create faster than ever. But this results in problems elsewhere. Sure you can push multiple PRs, but do you have people to review them? Do you have confidence that your changes aren&apos;t going to break production? How fast can you even deploy the multiple PRs you&apos;re pushing? It&apos;s clear that writing code was never the bottleneck in the first place.&lt;/p&gt;&lt;p&gt;My takeaway from the year is that I&apos;m doubling down on the need for DevEx. By improving DevEx we improve so many other parts of the company. As hard as it is to quantify. Forsgren and Noda say it best:&lt;/p&gt;&lt;blockquote&gt;Poor DevEx creates a competitive disadvantage that compounds over time. &lt;/blockquote&gt;&lt;h2&gt;Defining what DevEx owns is hard&lt;/h2&gt;&lt;p&gt;Even after a year I&apos;m not sure I have a good answer to this question. Reducing friction for a developer could come in many forms. From low level infrastructure changes, like setting up a  staging environment. To high level component library to help devs build UI&apos;s faster. Where does a DevEx team sit in this range? &lt;/p&gt;&lt;p&gt;The best answer I&apos;ve come up with is that DevEx should be an organisation. It should have multiple teams specific to certain developer workflows e.g. CI, Monorepo tooling, Language upgrades or tooling, and best practices &amp;amp; learning.  It should also build on top of infrastructure tooling created by Platform Engineering teams. &lt;/p&gt;&lt;p&gt;Frictionless also touches on the relationship between platform engineering teams and DevEx. Platform engineering builds the infra, shared tooling, and services that enforce best practice and abstract away complexity. While DevEx teams take a broader view of friction across the entire developer lifecycle. A DevEx team may address problems that no platform solution can solve.&lt;/p&gt;&lt;p&gt;In my experience, I find this boundary not as clear cut. Especially as a growing company, platform or DevEx teams may not have the capacity to take on particular projects. And some projects that should’ve been on one team get started on a different one. This is where the lines get blurred. We do our best, is my current answer around ownership. &lt;/p&gt;&lt;h2&gt;DevEx should operate like a product team&lt;/h2&gt;&lt;p&gt;Devs are our customers. As an internal team, it feels strange to force &lt;em&gt;product&lt;/em&gt; thinking. But I&apos;ve found that having a strong product roadmap and vision for DevEx gives us more guidance and flexibility into the solutions we build. Instead of fighting fires across multiple dev teams we can think about solutions that work long term. We fix what the dev is really asking us to fix.&lt;/p&gt;&lt;p&gt;Also a note from an &lt;a href=&quot;https://overcommitted.dev/ep-35-decoding-developer-trends-inside-the-life-of-a-developer-focused-analyst-with-kate-holterhoff?ref=newsletter.jonathanyeong.com&quot;&gt;Overcommitted podcast episode with Kate Holterhoff&lt;/a&gt;. There is a wide range of “developers”. Or more specifically, people that use DevEx solutions. From feature teams, to quality engineering aka quality assurance teams, to even non-technical folks like designers or PMs. Treating DevEx like a product gives us space to think about different personas. &lt;/p&gt;&lt;p&gt;It also means, we need to stay close to our customers. For our team, we focused on talking to devs, observing them use the DevEx tooling, and surveys to gather feedback. I have noticed that if there’s some friction a dev will most likely try to solve it by themselves and move on. So it can be hard to know that something is actually wrong until you go talk to them.  &lt;/p&gt;&lt;p&gt;Finally, it should go without saying, but a DevEx team should never adopt an &lt;em&gt;us-versus-them &lt;/em&gt;mindset. &lt;/p&gt;&lt;h2&gt;Staffing DevEx is an interesting challenge&lt;/h2&gt;&lt;p&gt;I started this year thinking that DevEx team should consist of full stack developers. Boy was I wrong. My assumption was that our devs are full stack so we should have a team of full stack devs to support them. But it turns out there&apos;s so many parts to DevEx, that we need specialists. And that can be hard to hire for. &lt;/p&gt;&lt;p&gt;Something our team has started doing is offering rotations from feature developers. We work together to come up with a project that they&apos;re interested in, and something they can finish during the rotation. The idea is that they&apos;re getting to fix a pain point in their workflow, while also being able to affect change across the engineering organisation. And the DevEx team gets to tackle a feature that devs are asking for and can maintain this feature in the long term. Win Win!&lt;/p&gt;&lt;p&gt;There&apos;s a lot I&apos;m still figuring out. Like, how much abstraction is too much abstraction? Or what are the &lt;em&gt;right&lt;/em&gt; metrics to gather? Or what is the process of telling devs about DevEx changes (Slack sucks for announcements)? Still, it&apos;s been a fun year on the DevEx team. Lots of highs and lows and lessons along the way. If I could take one thing away from this experience, it&apos;s that I love working with devs. These are my people. &lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Loading Ghost CMS posts with Astro Content Collections</title><link>https://jonathanyeong.com/writing/loading-ghost-cms-posts-with-astro-content-collections/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/loading-ghost-cms-posts-with-astro-content-collections/</guid><description>A guide to how I created a custom Astro content collection loader to fetch content from Ghost CMS and load them alongside my existing markdown files</description><pubDate>Wed, 26 Nov 2025 13:31:07 GMT</pubDate><content:encoded>&lt;p&gt;I recently made the switch to using Ghost as a headless CMS and I needed to load posts into my Astro site. Along with the Ghost posts, I also wanted to continue loading my existing markdown files. One, because I was too lazy to migrate. And two, because I think it’ll be cool to create more interactive &lt;code&gt;mdx&lt;/code&gt; posts in the future. &lt;/p&gt;&lt;p&gt;Thankfully, using Astro&apos;s &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/?ref=newsletter.jonathanyeong.com&quot;&gt;Content Layer API and content collections&lt;/a&gt; it was straightforward to load content from various sources. And in this blog post, I&apos;ll share how I used content collections to load both Ghost posts and markdown files.&lt;/p&gt;&lt;h2&gt;What is a Content Collection&lt;/h2&gt;&lt;p&gt;Content collections are a way Astro manages sets of content. And the Content Layer API defines an interface to interact with said content. This lets us plug in multiple sources of content, like a usb stick, for us to interact with.&lt;/p&gt;&lt;p&gt;For example, I&apos;m using the in-built glob loader that Astro provides to load up my markdown files. I can also define a zod schema for typechecking. In &lt;code&gt;src/content.config.ts&lt;/code&gt;:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { defineCollection, z } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro:content&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { glob } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro/loaders&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; blog &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; defineCollection&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; glob&lt;/span&gt;&lt;span&gt;({ pattern&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;*.md&quot;&lt;/span&gt;&lt;span&gt;, base&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;src/content/blog&quot;&lt;/span&gt;&lt;span&gt; }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  schema&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// Transform string to Date object&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;((val) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(val)),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;((str) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; (str &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(str) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		heroImage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		featured&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;([]),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		bskyPostId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; collections &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { blog };&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I can then fetch content from this collection via the Content Layer API. In my &lt;code&gt;src/pages/writing/[slug].astro&lt;/code&gt; file:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import { getCollection, render } from &apos;astro:content&apos;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export async function getStaticPaths() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const posts = await getCollection(&apos;blog&apos;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return posts.map((post) =&amp;gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		params: { slug: post.id },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		props: { post },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const { post } = Astro.props;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const { Content } = await render(post);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;h1&amp;gt;{post.data.title}&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Content /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With these two files I&apos;m able to generate static paths based on the files loaded by the &lt;code&gt;glob&lt;/code&gt; loader and render their content. This snippet is copied from an &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/?ref=newsletter.jonathanyeong.com#building-for-static-output-default&quot;&gt;Astro guide&lt;/a&gt; with some minor tweaks. &lt;/p&gt;&lt;p&gt;To get this working with Ghost, I need add a few more files.&lt;/p&gt;&lt;h2&gt;Loading Ghost Posts&lt;/h2&gt;&lt;p&gt;Before I dive into my custom Ghost loader, I want to give a shoutout to MatthiesenXYZ for creating the &lt;a href=&quot;https://github.com/MatthiesenXYZ/ghostcms-loader?ref=newsletter.jonathanyeong.com&quot;&gt;ghostcms-loader&lt;/a&gt;. This loader uses &lt;a href=&quot;https://github.com/PhilDL/ts-ghost/tree/main?ref=newsletter.jonathanyeong.com&quot;&gt;ts-ghost&lt;/a&gt; under the hood, and contains far more functionality than I introduced in my custom loader. The downside of this package is that it&apos;s pinned to Ghost v5 API, and for my usecase it seemed like it was overkill. For example, I only want to load posts right now and not other parts of Ghost.&lt;/p&gt;&lt;p&gt;Secondly, the &lt;a href=&quot;https://docs.astro.build/en/guides/cms/ghost/?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost + Astro setup guide&lt;/a&gt; is very good and I had most things setup except the custom loader after walking through the guide. I&apos;ll be building off the foundation that the guide set.&lt;/p&gt;&lt;h2&gt;Creating the Ghost loader&lt;/h2&gt;&lt;p&gt;Instead of &lt;code&gt;src/lib/ghost.ts&lt;/code&gt; from the Ghost + Astro setup guide. I created two files, one named &lt;code&gt;src/lib/api/ghost.ts&lt;/code&gt; and the other &lt;code&gt;src/lib/loaders/ghostPosts.ts&lt;/code&gt;&lt;/p&gt;&lt;p&gt;The &lt;code&gt;api/ghost.ts&lt;/code&gt; file setup a client using the &lt;a href=&quot;https://docs.ghost.org/content-api/javascript?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost content api&lt;/a&gt; and the file content is similar to the guide except it specifies the v6 API version and points to my production site. I also followed the &lt;a href=&quot;https://docs.astro.build/en/guides/cms/ghost/?ref=newsletter.jonathanyeong.com#setting-up-credentials&quot;&gt;guide to setup the &lt;code&gt;CONTENT_API_KEY&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; GhostContentAPI &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;@tryghost/content-api&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; ghostClient &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; GhostContentAPI&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    url&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;https://&amp;lt;YOUR-GHOST-SITE&amp;gt;.ghost.io&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    key&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CONTENT_API_KEY,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;v6.0&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next up, I created the custom loader that calls &lt;code&gt;ghostClient&lt;/code&gt;. In &lt;code&gt;src/lib/loaders/ghostPosts.ts&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt; { Loader, LoaderContext } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro/loaders&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { ghostClient } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;@lib/api/ghost&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; extractBskyPostId&lt;/span&gt;&lt;span&gt;(codeInjection&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; match&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; codeInjection&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&amp;lt;!--\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;METADATA:\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;bskyPostId=&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;a-zA-Z0-9&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;--&amp;gt;/&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; match?.[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; ghostPostLoader&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Loader&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;ghostcms-posts&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    load&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;span&gt; store,&lt;/span&gt;&lt;span&gt; logger,&lt;/span&gt;&lt;span&gt; parseData&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; LoaderContext&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        let&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        let&lt;/span&gt;&lt;span&gt; hasMore&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        const&lt;/span&gt;&lt;span&gt; allPosts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Starting to fetch Ghost posts...&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        while&lt;/span&gt;&lt;span&gt; (hasMore)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; posts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; ghostClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;browse&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            limit&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; page,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            include&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;tags&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          allPosts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;posts);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; nextPage&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;next;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          hasMore&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; nextPage&lt;/span&gt;&lt;span&gt; !==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          if&lt;/span&gt;&lt;span&gt; (nextPage&lt;/span&gt;&lt;span&gt; !==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            page&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; nextPage;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Fetched page &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; of &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pages&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Loaded &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;allPosts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; posts from Ghost`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt; of&lt;/span&gt;&lt;span&gt; allPosts)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; id&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; bskyPostId&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; extractBskyPostId&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;codeinjection_foot&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          // Transformation done here to match the schema of our md blog collection.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; rawData&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;custom_excerpt&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;excerpt&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;published_at&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt;  new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;published_at)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updated_at&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updated_at)&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            heroImage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;feature_image&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; // Ghost posts are published by default&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            featured&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;featured&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags?.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(tag&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; tag&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name)&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; [],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            readingTime&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reading_time,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            bskyPostId,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          const&lt;/span&gt;&lt;span&gt; parsedData&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; parseData&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; rawData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; parsedData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            rendered&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              html&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; ||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Ghost posts loaded successfully&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;span&gt; catch&lt;/span&gt;&lt;span&gt; (error)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Ghost loader error: &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; instanceof&lt;/span&gt;&lt;span&gt; Error&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; error&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;Unknown error&apos;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        throw&lt;/span&gt;&lt;span&gt; error;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that’s a lot of code, so let&apos;s break it down.&lt;/p&gt;&lt;h3&gt;Satisfying the Loader Interface &lt;/h3&gt;&lt;p&gt;Firstly, every &lt;code&gt;Loader&lt;/code&gt; has an &lt;a href=&quot;https://docs.astro.build/en/reference/content-loader-reference/?ref=newsletter.jonathanyeong.com#the-loader-object&quot;&gt;interface we must meet&lt;/a&gt;. It must return a &lt;code&gt;name&lt;/code&gt; of the loader, and a &lt;code&gt;load&lt;/code&gt; function. Optionally, it can return a schema as well. But for my case, I&apos;ll be defining the schema in &lt;code&gt;content.config.ts&lt;/code&gt;&lt;/p&gt;&lt;p&gt;Here’s all we need to satisfy the &lt;code&gt;Loader&lt;/code&gt; interface:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; ghostPostLoader&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Loader&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;ghostcms-posts&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    load&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;span&gt; store,&lt;/span&gt;&lt;span&gt; logger,&lt;/span&gt;&lt;span&gt; parseData&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; LoaderContext&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}    &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;load&lt;/code&gt; method takes a &lt;code&gt;LoaderContext&lt;/code&gt; which contains various props but the ones I use are &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;logger&lt;/code&gt;, and &lt;code&gt;parseData&lt;/code&gt;.&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;code&gt;store&lt;/code&gt; is used to store the actual data (which we&apos;ll go over below).&lt;/li&gt;&lt;li&gt;&lt;code&gt;logger&lt;/code&gt; is logs messages to the console. But the logs also contain the loader name for easier debugging.&lt;/li&gt;&lt;li&gt;&lt;code&gt;parseData&lt;/code&gt; validates the data according to the collection schema, which I defined outside the loader in &lt;code&gt;content.config.ts&lt;/code&gt; (more on that below).&lt;/li&gt;&lt;/ul&gt;&lt;h3&gt;Calling Ghost client to load posts&lt;/h3&gt;&lt;p&gt;Within the &lt;code&gt;load&lt;/code&gt; method, I call &lt;code&gt;ghostClient&lt;/code&gt; to load the posts:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  let&lt;/span&gt;&lt;span&gt; page &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  let&lt;/span&gt;&lt;span&gt; hasMore &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; allPosts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Starting to fetch Ghost posts...&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  while&lt;/span&gt;&lt;span&gt; (hasMore) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; ghostClient&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;browse&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      limit&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 100&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; page,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      include&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;tags&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    allPosts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;push&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;posts);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; nextPage &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;next;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    hasMore &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; nextPage &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; (nextPage &lt;/span&gt;&lt;span&gt;!==&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      page &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; nextPage;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;info&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Fetched page &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;page&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; of &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pagination&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pages&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (error) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    logger&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Ghost loader error: &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt; instanceof&lt;/span&gt;&lt;span&gt; Error&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; error&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;Unknown error&apos;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    throw&lt;/span&gt;&lt;span&gt; error;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In Ghost API v6, they removed &lt;code&gt;limit: all&lt;/code&gt; meaning we are forced to paginate over posts. Also, &lt;code&gt;ghostClient.posts&lt;/code&gt; fetches posts from the &lt;a href=&quot;https://docs.ghost.org/content-api/javascript?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost Content API&lt;/a&gt; which will &lt;strong&gt;only&lt;/strong&gt; ever return &lt;code&gt;published&lt;/code&gt; posts. Making it impossible to preview &lt;code&gt;draft&lt;/code&gt; posts locally. When setting Ghost up, I published a couple of posts so I could see them pulled locally. It&apos;s annoying but not the end of the world.&lt;/p&gt;&lt;p&gt;I also &lt;code&gt;include: [&apos;tags&apos;]&lt;/code&gt; that I convert into &lt;code&gt;topics&lt;/code&gt; which are my blogs representation of tags. For example,&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags?.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(tag &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; tag&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; [],&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Transforming the output&lt;/h3&gt;&lt;p&gt;This final step is optional. But in my workflow, I wanted to transform the output of the API call to match what my markdown file frontmatter would return. This meant I could re-use the schema declaration in &lt;code&gt;content.config.ts&lt;/code&gt; between my markdown files and ghost posts.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; post &lt;/span&gt;&lt;span&gt;of&lt;/span&gt;&lt;span&gt; allPosts) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; id &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; bskyPostId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; extractBskyPostId&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;codeinjection_foot &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  // Transformation done here to match the schema of our md blog collection.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; rawData &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;title,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;custom_excerpt &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;excerpt &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;published_at &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt;  new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;published_at)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updated_at &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;updated_at) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    heroImage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;feature_image &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;// Ghost posts are published by default&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    featured&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;featured &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;tags?.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;(tag &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; tag&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; [],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    readingTime&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reading_time,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    bskyPostId,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; parsedData &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; parseData&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; rawData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  store&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;set&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    data&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; parsedData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    rendered&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      html&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;html &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;One thing to note is, &lt;code&gt;const id = post.slug&lt;/code&gt;. Ghost slugs are unique so it&apos;s okay setting this as ID. It also matches what &lt;code&gt;glob&lt;/code&gt; loader does, where &lt;code&gt;id&lt;/code&gt; is the slug-ified markdown filename.&lt;/p&gt;&lt;p&gt;After transforming the data we save it to the data store:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;store.set({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  id,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  data: parsedData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  rendered: {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    html: post.html || &apos;&apos;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;store&lt;/code&gt; is a key value store scoped to the collection. Meaning the &lt;code&gt;id&lt;/code&gt; must be unique otherwise you&apos;ll be returning duplicate entries when you fetch the content collection. That&apos;s why it&apos;s important that the Ghost slugs are unique.&lt;/p&gt;&lt;p&gt;It sets the id of the post (aka the slug) to the &lt;code&gt;parsedData&lt;/code&gt; which matches what is defined in the content schema. It also accepts a &lt;code&gt;rendered&lt;/code&gt; prop which is set to the &lt;code&gt;post.html&lt;/code&gt;. This will make the &lt;code&gt;render()&lt;/code&gt; function available on the post in our Astro pages like this: &lt;code&gt;const { Content } = await render(post);&lt;/code&gt;&lt;/p&gt;&lt;h3&gt;Bluesky post ID&lt;/h3&gt;&lt;p&gt;You might have noticed this reference to &lt;code&gt;bskyPostId&lt;/code&gt;. This is how I tie a bluesky post to my blog post. And it&apos;s how I load the comments/interactions below! To do so I insert a code injection to the post footer that looks like this:&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;!-- METADATA: bskyPostId=3m6g6tkputk2j --&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will then be extracted and parsed by this &lt;code&gt;extractBskyPostId&lt;/code&gt; function. Which takes &lt;code&gt;post.codeinjection_foot&lt;/code&gt; which is a field found on the &lt;code&gt;post&lt;/code&gt; object from Ghost.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; extractBskyPostId&lt;/span&gt;&lt;span&gt;(codeInjection&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; match&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; codeInjection&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;match&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&amp;lt;!--\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;METADATA:\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;bskyPostId=&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;span&gt;a-zA-Z0-9&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;\s&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;--&amp;gt;/&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; match?.[&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; bskyPostId &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; extractBskyPostId&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;codeinjection_foot &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At the end, I pass this &lt;code&gt;bskyPostId&lt;/code&gt; to the &lt;code&gt;rawData&lt;/code&gt; and that field can be used throughout my site. One of the downsides of working with Ghost is the inability to define custom metadata. This is a quick workaround to that problem and something that Astro content collections makes simple.&lt;/p&gt;&lt;h3&gt;Content config changes&lt;/h3&gt;&lt;p&gt;Now that I&apos;ve setup the Ghost Post loader. I can modify my &lt;code&gt;content.config.ts&lt;/code&gt; to export the new collection, reusing the &lt;code&gt;postSchema&lt;/code&gt; across both the existing markdown posts and the new Ghost posts.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { defineCollection, z } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro:content&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { glob } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro/loaders&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { ghostPostLoader } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;@lib/loaders/ghostPosts&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; postSchema &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;object&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		// Transform string to Date object&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;((val) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(val)),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;or&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;transform&lt;/span&gt;&lt;span&gt;((str) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; (str &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;(str) &lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; undefined&lt;/span&gt;&lt;span&gt;)),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		heroImage&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		featured&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			.&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;([]),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		bskyPostId&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		readingTime&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;number&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; blog &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; defineCollection&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; glob&lt;/span&gt;&lt;span&gt;({ pattern&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;*.md&quot;&lt;/span&gt;&lt;span&gt;, base&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;src/content/blog&quot;&lt;/span&gt;&lt;span&gt; }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	schema&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postSchema&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; ghostCmsPosts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; defineCollection&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  loader&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ghostPostLoader&lt;/span&gt;&lt;span&gt;(),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	schema&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postSchema&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; collections &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; { blog, ghostCmsPosts };&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Fetching posts side by side&lt;/h2&gt;&lt;p&gt;Finally, how do I actually fetch the posts? I created a utility &lt;code&gt;src/utilities/getAllPosts.ts&lt;/code&gt; to load the two content collections side by side.&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { getCollection } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro:content&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt; { CollectionEntry } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro:content&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt; BlogPost&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;blog&apos;&lt;/span&gt;&lt;span&gt;&amp;gt; &lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&apos;ghostCmsPosts&apos;&lt;/span&gt;&lt;span&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; default&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; getAllPosts&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;BlogPost&lt;/span&gt;&lt;span&gt;[]&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; blogPosts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;blog&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; ghostPosts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;ghostCmsPosts&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; allPosts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;blogPosts,&lt;/span&gt;&lt;span&gt; ...&lt;/span&gt;&lt;span&gt;ghostPosts];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; allPosts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;((a,&lt;/span&gt;&lt;span&gt; b)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; dateA&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate?.&lt;/span&gt;&lt;span&gt;getTime&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; ??&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; dateB&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate?.&lt;/span&gt;&lt;span&gt;getTime&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; ??&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; dateB&lt;/span&gt;&lt;span&gt; -&lt;/span&gt;&lt;span&gt; dateA;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And now I can use this utility to fetch all of my posts! Here&apos;s the updated &lt;code&gt;src/pages/writing/[slug].astro&lt;/code&gt;&lt;/p&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import { render } from &apos;astro:content&apos;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import getAllPosts from &apos;@utilities/getAllPosts&apos;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export async function getStaticPaths() {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const posts = await getAllPosts();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return posts.map((post) =&amp;gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		params: { slug: post.id },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		props: { post },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	}));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const { post } = Astro.props;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const { Content } = await render(post);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;h1&amp;gt;{post.data.title}&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;Content /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;You made it!&lt;/h2&gt;&lt;p&gt;Thanks for making it this far! I hope this tutorial helps you create a custom loader. And more specifically how to set up a Ghost loader. Feel free to check out my &lt;a href=&quot;https://github.com/jonathanyeong/personal-website/pull/64?ref=newsletter.jonathanyeong.com&quot;&gt;PR for the changes in context&lt;/a&gt;. And if you have any feedback please let me know!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Mobile-Friendly Headless CMS&apos;s with Astro</title><link>https://jonathanyeong.com/writing/mobile-friendly-headless-cmss-with-astro/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/mobile-friendly-headless-cmss-with-astro/</guid><description>I loved my Obsidian blog workflow, but I wanted a better mobile publishing workflow. To change things up, I tested four headless CMSs with Astro: Sanity, Ghost, TinaCMS, and PagesCMS. In the end, I chose Ghost. Here&apos;s why I chose it, and where it falls short.</description><pubDate>Sun, 23 Nov 2025 14:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A while back, I set up &lt;a href=&quot;https://jonathanyeong.com/writing/writing-blog-posts-with-obsidian/?ref=newsletter.jonathanyeong.com&quot;&gt;Obsidian for my blog post workflow&lt;/a&gt;. There are many great things about it; the editor is minimal and distraction free. My notes and blog posts all lived in one app. And I felt like a hacker setting up GitHub actions, symlinks, and custom plugins to automate git pushes. &lt;/p&gt;&lt;p&gt;But there was one thing I felt missing. A mobile publishing experience. I&apos;ve been using &lt;a href=&quot;https://dayoneapp.com/?ref=newsletter.jonathanyeong.com&quot;&gt;Day One&lt;/a&gt; lately to journal and I&apos;ve loved writing a quick entry while waiting at the coffee shop. Or while sitting on a bench in the park with my pup. I wanted to bring that to my blog post workflow. Anything to reduce the friction of hitting &quot;publish&quot;.&lt;/p&gt;&lt;p&gt;To achieve this, I wanted to use a Headless CMS. I tested four:  &lt;a href=&quot;https://www.sanity.io/?ref=newsletter.jonathanyeong.com&quot;&gt;Sanity&lt;/a&gt;, &lt;a href=&quot;https://ghost.org/?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost&lt;/a&gt;, &lt;a href=&quot;https://tina.io/?ref=newsletter.jonathanyeong.com&quot;&gt;TinaCMS&lt;/a&gt;, and &lt;a href=&quot;https://pagescms.org/?ref=newsletter.jonathanyeong.com&quot;&gt;PagesCMS&lt;/a&gt;. I chose based on some loose criteria:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Popular (reddit threads, discussed in blog posts, bluesky mentions)&lt;/li&gt;&lt;li&gt;Open source&lt;/li&gt;&lt;li&gt;Either File-based (TinaCMS, PagesCMS) or DB-based (Sanity, Ghost)&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;In the end, I decided to go with &lt;a href=&quot;https://ghost.org/?ref=newsletter.jonathanyeong.com&quot;&gt;Ghost&lt;/a&gt;. Here&apos;s why I chose it over the others.&lt;/p&gt;&lt;h2&gt;Why Ghost?&lt;/h2&gt;&lt;h3&gt;Mobile editing is the dealbreaker&lt;/h3&gt;&lt;p&gt;While I still do most of my writing on desktop, I wanted a great mobile friendly user experience for publishing. And I liked Ghost&apos;s experience the most out of the CMSs. It has a clean canvas when you load up a new post. It&apos;s a distraction free writing experience that lets you focus on the content not the &lt;em&gt;structure &lt;/em&gt;of your blog post.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://newsletter.jonathanyeong.com/content/images/2025/11/ghost-mobile.png&quot; alt=&quot;Ghost mobile views (left is editing a post, middle is post settings, right is the Posts view). The views are mobile friendly with the editing post view a canvas where you can just start writing.&quot; loading=&quot;lazy&quot; width=&quot;1881&quot; height=&quot;1356&quot; srcset=&quot;https://newsletter.jonathanyeong.com/content/images/size/w600/2025/11/ghost-mobile.png 600w, https://newsletter.jonathanyeong.com/content/images/size/w1000/2025/11/ghost-mobile.png 1000w, https://newsletter.jonathanyeong.com/content/images/size/w1600/2025/11/ghost-mobile.png 1600w, https://newsletter.jonathanyeong.com/content/images/2025/11/ghost-mobile.png 1881w&quot; /&gt;&lt;figcaption&gt;&lt;span&gt;Ghost mobile views &lt;/span&gt; &lt;span&gt;(left is editing a post, middle is post settings, right is the Posts view)&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;In comparison, the other CMSs editors felt clunky.&lt;/p&gt;&lt;figure&gt;&lt;img src=&quot;https://newsletter.jonathanyeong.com/content/images/2025/11/cms-mobile.png&quot; alt=&quot;From left to right; Sanity CMS, Pages CMS, Tina CMS mobile editing experience&quot; loading=&quot;lazy&quot; width=&quot;1881&quot; height=&quot;1356&quot; srcset=&quot;https://newsletter.jonathanyeong.com/content/images/size/w600/2025/11/cms-mobile.png 600w, https://newsletter.jonathanyeong.com/content/images/size/w1000/2025/11/cms-mobile.png 1000w, https://newsletter.jonathanyeong.com/content/images/size/w1600/2025/11/cms-mobile.png 1600w, https://newsletter.jonathanyeong.com/content/images/2025/11/cms-mobile.png 1881w&quot; /&gt;&lt;figcaption&gt;&lt;span&gt;From left to right; Sanity CMS, Pages CMS, Tina CMS mobile editing experience&lt;/span&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;At the time of writing, Sanity is experimenting with a &lt;a href=&quot;https://www.sanity.io/canvas?ref=newsletter.jonathanyeong.com&quot;&gt;Canvas&lt;/a&gt; similar to Ghost&apos;s editing experience. But to translate the Canvas content you need to use an experimental &lt;a href=&quot;https://www.sanity.io/docs/canvas/canvas-content-mapping?ref=newsletter.jonathanyeong.com&quot;&gt;Content Mapping feature&lt;/a&gt;.&lt;/p&gt;&lt;h3&gt;Straightforward Ghost CMS setup&lt;/h3&gt;&lt;p&gt;Setting up Ghost was straightforward. I followed the &lt;a href=&quot;https://docs.astro.build/en/guides/cms/ghost/?ref=newsletter.jonathanyeong.com&quot;&gt;Astro + Ghost setup guide&lt;/a&gt; with a Ghost trial account and got something working in under an hour. In comparison setting up &lt;a href=&quot;https://www.sanity.io/docs/developer-guides/sanity-astro-blog?ref=newsletter.jonathanyeong.com&quot;&gt;Sanity felt much longer&lt;/a&gt;. And honestly, Sanity felt like overkill for a solo developer blog. Both TinaCMS and PagesCMS were straightforward to setup too. In many ways they matched my workflow better with existing blog posts in markdown files. But when it came to images I needed more work to get them behind a CDN (through cloudinary or aws for example).&lt;/p&gt;&lt;h3&gt;Newsletter capabilities ✨&lt;/h3&gt;&lt;p&gt;What pushed me over the edge was Ghost&apos;s newsletter capabilities. I could have my blog post content and my newsletter being managed by a single platform. I&apos;m currently using Beehiiv to send newsletters. And while I haven&apos;t written a newsletter in a while, consolidating content to one platform is very enticing!&lt;/p&gt;&lt;h2&gt;Why not Ghost?&lt;/h2&gt;&lt;p&gt;There are two reasons why Ghost might not be the right fit for you: cost and CMS limitations.&lt;/p&gt;&lt;h3&gt;Ghost Pro is Expensive&lt;/h3&gt;&lt;p&gt;Ghost Pro is the hosted version. To use it as a headless CMS, you need to subscribe to the &lt;a href=&quot;https://ghost.org/pricing/?ref=newsletter.jonathanyeong.com&quot;&gt;Publisher plan&lt;/a&gt;. This plan is $29 USD a month 😱 which is very expensive. Especially, if all you want is to author blog post content. Ghost does offer a self hosted alternative that will bring the cost down. But that brings additional setup and long term maintenance.&lt;/p&gt;&lt;h3&gt;CMS limitations&lt;/h3&gt;&lt;p&gt;Ghost in comparison to the other three CMSs was more limited in what you could do. For example, with the other three CMSs you could define &lt;em&gt;whatever&lt;/em&gt; schema you wanted for your blog post. And with Sanity and TinaCMS you could also enable live preview editing. Not so with Ghost. But I&apos;m okay with Ghost&apos;s constraints. It makes setup simpler.&lt;/p&gt;&lt;h2&gt;To CMS or not to CMS&lt;/h2&gt;&lt;p&gt;That is the question. At the end of the day, I think we can all agree that as a solo dev posting a handful of posts a year I don&apos;t need a CMS. You can get far with markdown files. &lt;/p&gt;&lt;p&gt;What about my old Obsidian setup? It&apos;s still there. I&apos;m honestly too lazy to migrate those markdown files to Ghost. And at some point might want to author an mdx post that is more customizable than Ghost. Thankfully, with &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/?ref=newsletter.jonathanyeong.com#defining-collections&quot;&gt;Astro content collections&lt;/a&gt; it&apos;s easy to load both markdown files and your CMS content. So my advice: give both approaches a try and see what works best.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Redesigning my site: version 3</title><link>https://jonathanyeong.com/writing/blog-redesign-v3/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/blog-redesign-v3/</guid><description>A behind-the-scenes look at redesigning my personal site (v3). I share my design goals, evolution through multiple iterations, and key technical changes.</description><pubDate>Fri, 18 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;After a months-long redesign with some stops and starts, I’m psyched to finally share my new site with you!&lt;/p&gt;
&lt;p&gt;I’m a sucker for behind the scenes posts. I love reading them. I love writing them. I was particularly inspired by the &lt;a href=&quot;https://piccalil.li/blog/redesigning-piccalilli-the-first-part-of-the-design-process/&quot; target=&quot;_blank&quot;&gt;Redesigning Piccalilli&lt;/a&gt; series. So I thought it would be interesting to go over my goals for the design, the design process, and key moments in the build.&lt;/p&gt;
&lt;h2&gt;Goals for the design&lt;/h2&gt;
&lt;p&gt;Before jumping into Figma, I wrote down some goals for the redesign.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The main purpose of this site is a blog. Posts should be easy to read and discover, with a touch of personal curation (recommended or featured posts).&lt;/li&gt;
&lt;li&gt;I want a site that’s personal and friendly yet clean and minimal - but not so minimal that it’s boring.&lt;/li&gt;
&lt;li&gt;Big type (for my old man eyes), splashes of colour maybe something green.&lt;/li&gt;
&lt;li&gt;Newsletter CTA. I’d love to have more people find my newsletter.&lt;/li&gt;
&lt;li&gt;A design I can build in a reasonable amount of time, and extensible in the future.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Overall, I’m happy with the result of the redesign. Achieving the second goal is the hardest challenge. Creating something personal and friendly while also being minimal seems like a contradiction. I’m already thinking of changes to the design to bring more personality to it in the future.&lt;/p&gt;
&lt;h2&gt;Design Phase&lt;/h2&gt;
&lt;p&gt;During the design phase, I had three design iterations. The first iteration I finished at the end of 2024. The last I finished sometime last month.&lt;/p&gt;
&lt;p&gt;In the first iteration, I wanted to display more information so that you could understand who I was at a glance. As well as introduce some sort of post curation. I was tired of the plain old homepage, so I added more colours and photos. I was also really into gradients last year.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081709/unsigned_obsidian_uploads/jligyhokw4lb7ffnenmp.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081709/unsigned_obsidian_uploads/jligyhokw4lb7ffnenmp.png&quot; alt=&quot;first iteration of my personal site redesign&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The second iteration went a completely different direction. I switched to warmer colours, opting for something more minimal. I also switched out the heading font to &lt;a href=&quot;https://fonts.google.com/specimen/Poppins&quot; target=&quot;_blank&quot;&gt;Poppins&lt;/a&gt; on the homepage. I really love how round and friendly it feels.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081724/unsigned_obsidian_uploads/igsnz8gk9uesihkjtgtd.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081724/unsigned_obsidian_uploads/igsnz8gk9uesihkjtgtd.png&quot; alt=&quot;second iteration of my personal site redesign&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Unfortunately, both the first and second iterations were difficult to build. And when I stopped to think about it, neither design checked all my goals.&lt;/p&gt;
&lt;p&gt;Which brings us to the final iteration!&lt;/p&gt;
&lt;p&gt;I changed out the hero because it just didn’t work when the screen was super wide. Instead, I took inspiration from the original introduction paragraph and turned it into a hero. I also made the About section an actual page. I’d like to extend that page in the future.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081737/unsigned_obsidian_uploads/aj0rfztpwahdazg67cxh.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081737/unsigned_obsidian_uploads/aj0rfztpwahdazg67cxh.png&quot; alt=&quot;Third and final iteration of my personal site redesign&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One thing I love about Figma is how easy it is to preserve your iteration process. Here’s what it looked like designing from a blank page to the second and final iteration. It’s not the most efficient design process but it looks neat!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745070805/unsigned_obsidian_uploads/jkstqh5astbmazuuzyqu.gif&quot; alt=&quot;Gif of design process between iteration one and two&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Build Phase&lt;/h2&gt;
&lt;p&gt;During the build phase, I deviated a little from my design. Thankfully, I don’t need to be perfect during design because I’m the one building it.&lt;/p&gt;
&lt;p&gt;Before I highlight some key changes in this build, I need to undo my past decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I removed Tailwind and rolled my own CSS. No shade against Tailwind, but I want to learn CSS, not Tailwind.&lt;/li&gt;
&lt;li&gt;I reintroduced my blog posts hierarchy with &lt;code&gt;/writing&lt;/code&gt;. I thought it looked cool to have a flat hierarchy, but it felt awkward with file based routing.&lt;/li&gt;
&lt;li&gt;I retired my logo that I’ve had for years. I felt like it clashed with the new design. Although, it still lives on as a favicon.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Light Dark Theme&lt;/h3&gt;
&lt;p&gt;It’s been a while since I’ve written CSS. And there’s been so many cool things since I’ve last touched it. The CSS &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;light-dark&lt;/code&gt; function&lt;/a&gt; is one of those things.&lt;/p&gt;
&lt;p&gt;In my stylesheet, I set the &lt;code&gt;:root&lt;/code&gt; pseudoclass to use the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme&quot; target=&quot;_blank&quot;&gt;light dark color scheme&lt;/a&gt; along with some light-dark variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;root&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	color-scheme&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; light dark&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	--color-text&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; light-dark(var&lt;/span&gt;&lt;span&gt;(--color-dark-text)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;(--color-light-text))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	--color-bg&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; light-dark(var&lt;/span&gt;&lt;span&gt;(--color-light-bg)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;(--color-dark-bg))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To implement the theme switcher, I first check if there’s a theme in local storage. If there’s not, then I get the browser theme. I toggle the theme, and set &lt;code&gt;color-scheme&lt;/code&gt; to the new theme, as well as update the local storage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; preferredColorScheme &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (() &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; window&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matchMedia&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;(prefers-color-scheme: dark)&apos;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;matches &lt;/span&gt;&lt;span&gt;?&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;light&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; handleToggleClick&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; currentTheme &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; localStorage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; preferredColorScheme&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    const&lt;/span&gt;&lt;span&gt; newTheme &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; currentTheme &lt;/span&gt;&lt;span&gt;===&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;span&gt; ?&lt;/span&gt;&lt;span&gt; &apos;light&apos;&lt;/span&gt;&lt;span&gt; :&lt;/span&gt;&lt;span&gt; &apos;dark&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;documentElement&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;style&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;color-scheme&apos;&lt;/span&gt;&lt;span&gt;, newTheme)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    localStorage&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setItem&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;theme&apos;&lt;/span&gt;&lt;span&gt;, newTheme);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also wanted to make sure my code blocks switch between dark and light mode. Under the hood, &lt;a href=&quot;https://docs.astro.build/en/guides/syntax-highlighting/#markdown-code-blocks&quot; target=&quot;_blank&quot;&gt;Astro uses Shiki to parse markdown code blocks&lt;/a&gt;. And it has hooks to modify the Shiki config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// In astro.config.js&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;shikiConfig&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  themes&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	dark&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;everforest-dark&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	light&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;everforest-light&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  defaultColor&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;},&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s important to set &lt;code&gt;defaultColor&lt;/code&gt; to false. This forces Shiki to generate a token with both light and dark variables. Example from &lt;a href=&quot;https://shiki.style/guide/dual-themes#light-dark-dual-themes&quot; target=&quot;_blank&quot;&gt;Shiki docs&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; style&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;--shiki-dark:#D8DEE9;--shiki-light:#2E3440&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;console&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in my CSS, I use &lt;code&gt;light-dark&lt;/code&gt; function to switch between the two variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;astro-code&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;astro-code&lt;/span&gt;&lt;span&gt; span&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; light-dark(var&lt;/span&gt;&lt;span&gt;(--shiki-light)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;(--shiki-dark))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  background-color&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; light-dark(var&lt;/span&gt;&lt;span&gt;(--shiki-light-bg)&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; var&lt;/span&gt;&lt;span&gt;(--shiki-dark-bg))&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way a user can manually toggle the theme, or set their browser appearance and the code blocks will change colours correctly.&lt;/p&gt;
&lt;h3&gt;Animating the Hero text&lt;/h3&gt;
&lt;p&gt;I wanted to try my hand at animating some text using only CSS. It’s a small experiment to get myself more comfortable with animations.&lt;/p&gt;
&lt;p&gt;First, let’s start with the HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;animate-phrase&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Hey I&apos;m Jono.&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;animate-phrase&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Backend developer.&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;animate-phrase&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Learning to build delightful websites.&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;animate-phrase&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Telling stories along the way.&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;animate-phrase&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Obsessed about developer experience.&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;span&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I wanted to have each span slide up on load, staggered from each other. To achieve this animation, my CSS follows these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Setup the initial state of the text.&lt;/li&gt;
&lt;li&gt;Define a key frame that transitions from the initial to final state.&lt;/li&gt;
&lt;li&gt;For each &lt;code&gt;animate-phrase&lt;/code&gt;, add an animation with a slight delay to stagger each phrase.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By setting the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/animation-fill-mode&quot; target=&quot;_blank&quot;&gt;animation-fill-mode&lt;/a&gt; to &lt;code&gt;forwards&lt;/code&gt; my &lt;code&gt;animate-phrase&lt;/code&gt; class will retain the properties of the last keyframe.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;media&lt;/span&gt;&lt;span&gt; (prefers-reduced-motion&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; no-preference) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	  display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; inline-block&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; translateY&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    @keyframes&lt;/span&gt;&lt;span&gt; slideUp&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      0% {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; translateY&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      100% {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        transform&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; translateY&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;nth-child&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; slideUp 500&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; ease-out 0&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; 1 forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;nth-child&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; slideUp 500&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; ease-out 100&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; 1 forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;nth-child&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;3&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; slideUp 500&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; ease-out 200&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; 1 forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;nth-child&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;4&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; slideUp 500&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; ease-out 300&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; 1 forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;animate-phrase&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt;nth-child&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      animation&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; slideUp 500&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; ease-out 400&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt; 1 forwards&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Writing out the CSS transition wasn’t too bad. But I can see why you would switch to using a JS library.&lt;/p&gt;
&lt;p&gt;It’s a pain to manage the delay for the stagger. If I were using a JS animation library like &lt;a href=&quot;https://animejs.com/&quot; target=&quot;_blank&quot;&gt;anime.js&lt;/a&gt;, it could look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { createTimeline, stagger } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;animejs&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;DOMContentLoaded&apos;&lt;/span&gt;&lt;span&gt;, () &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; timeline &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; createTimeline&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	timeline&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	  .&lt;/span&gt;&lt;span&gt;add&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;.animate-phrase&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		opacity&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		translateY&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		duration&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 500&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		delay&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; stagger&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	  })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Subscribing users to my newsletter&lt;/h3&gt;
&lt;p&gt;Finally, I got to write some backend code 🤩.&lt;/p&gt;
&lt;p&gt;Beehiiv has an API you can use to subscribe emails to your newsletter. I needed to create a server side endpoint in Astro to call this API.&lt;/p&gt;
&lt;p&gt;First step is to create a &lt;code&gt;pages/api/subscribe.ts&lt;/code&gt; file that calls the Beehiiv endpoint.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;For brevity, I omitted validations and error checking in the following code blocks.&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt; { APIRoute } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; prerender &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt; POST&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; APIRoute&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; ({ request }) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; formData &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; request&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;formData&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; email &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; formData&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;email&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; beehiivUrl &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; `https://api.beehiiv.com/v2/publications/&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;BEEHIIV_PUBLICATION_ID&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;/subscriptions`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	const&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(beehiivUrl, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		headers&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		  &quot;Content-Type&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;application/json&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		  &quot;Authorization&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; `Bearer &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;BEEHIIV_API_KEY&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		},&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		body&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({ email }),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Response&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		JSON&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;stringify&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			message&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;Success! Check your email to confirm!&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		}),&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		{ status&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 200&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding &lt;code&gt;export const prerender = false&lt;/code&gt; tells Astro that this file should be server side rendered.&lt;/p&gt;
&lt;p&gt;Next in my &lt;code&gt;Newsletter.astro&lt;/code&gt; component, I set up the client side code to call this backend endpoint.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; formElement &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; document&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;newsletterForm&apos;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; HTMLFormElement&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;formElement&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;onsubmit&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; (e) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	e&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;preventDefault&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	try&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		const&lt;/span&gt;&lt;span&gt; formData &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; FormData&lt;/span&gt;&lt;span&gt;(formElement);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		const&lt;/span&gt;&lt;span&gt; email &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; formData&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;email&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		const&lt;/span&gt;&lt;span&gt; response &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;/api/subscribe&quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			method&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;POST&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;			body&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; formData,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		});&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;		const&lt;/span&gt;&lt;span&gt; result &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;json&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        formElement&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;reset&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    } &lt;/span&gt;&lt;span&gt;catch&lt;/span&gt;&lt;span&gt; (error) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Error:&apos;&lt;/span&gt;&lt;span&gt;, error);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The last thing was to add the corresponding adapter to my Astro config. I’m using &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/netlify/&quot; target=&quot;_blank&quot;&gt;Netlify&lt;/a&gt; so I added &lt;code&gt;adapter: netlify()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Huge props to Astro for making hybrid rendering relatively easy! I love that I can statically render my &lt;code&gt;Newsletter.astro&lt;/code&gt; component but have it call a server side rendered route.&lt;/p&gt;
&lt;h2&gt;Before and after of the homepage&lt;/h2&gt;
&lt;p&gt;To wrap everything up, here are some before and after pictures of the homepage. It’s nice to see the difference at a glance!&lt;/p&gt;
&lt;h3&gt;Before&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081595/unsigned_obsidian_uploads/rgun7zqofb6drov5qzjl.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081595/unsigned_obsidian_uploads/rgun7zqofb6drov5qzjl.png&quot; alt=&quot;old homepage in light theme&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081595/unsigned_obsidian_uploads/hax3fqxxiid1h1t3hhok.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081595/unsigned_obsidian_uploads/hax3fqxxiid1h1t3hhok.png&quot; alt=&quot;old homepage in dark theme&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;After&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081613/unsigned_obsidian_uploads/ksmp2vngjyky5rlbxrw8.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081613/unsigned_obsidian_uploads/ksmp2vngjyky5rlbxrw8.png&quot; alt=&quot;new and current homepage in light theme&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081613/unsigned_obsidian_uploads/ilvkvwcdf82qtempygwu.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1745081613/unsigned_obsidian_uploads/ilvkvwcdf82qtempygwu.png&quot; alt=&quot;new and current homepage in dark theme&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I learnt so much throughout this redesign. It’s given me an appreciation of both design and frontend. That shit is hard! I already have a laundry list of things I want to add and tweak. But for now, the major part is done.&lt;/p&gt;
&lt;p&gt;I hope you like the new design! Let me know what you think. Shoot me an &lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>2024 Recapped</title><link>https://jonathanyeong.com/writing/2024-recapped/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/2024-recapped/</guid><description>In this post, I share my theme of this year: independence, how I tracked against my goals, and future plans.</description><pubDate>Thu, 26 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I love seeing people grow and overcome challenges. And at this time of year, I’m eating up all the 2024 wrapped posts I can find! It’s fascinating seeing what people cared about throughout the year.&lt;/p&gt;
&lt;p&gt;Without further ado, here’s my year recapped.&lt;/p&gt;
&lt;h2&gt;2024 Goals and Theme&lt;/h2&gt;
&lt;p&gt;I started off this year with a theme: &lt;strong&gt;Independence&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For context, the past couple of years have been a rollercoaster with work. I survived multiple layoffs at Shopify. Only to have the product, I was working on, sold to another company. Then I survived layoffs at that company. Only to find that the team I was working on being dissolved and moved offshore. Throughout this time, I felt like I had no power over my life.&lt;/p&gt;
&lt;p&gt;I wanted to own my own destiny, so I created some goals inspired by the theme. I have a more detailed list, but here are the most important ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get a new job.&lt;/li&gt;
&lt;li&gt;Start earning money on the side - freelance, or some other side hustle.&lt;/li&gt;
&lt;li&gt;Create content again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most challenging part of this year was managing my energy. The first half was spent looking for a job. And in the second half, I had bursts of content (like my &lt;a href=&quot;https://jonathanyeong.com/30-days-of-posts/&quot; target=&quot;_blank&quot;&gt;30 day writing challenge&lt;/a&gt;), followed by a long lull.&lt;/p&gt;
&lt;p&gt;But I was able to accomplish a lot this year. Much more than last year!&lt;/p&gt;
&lt;h2&gt;Achievements&lt;/h2&gt;
&lt;h3&gt;Got a new job ✅&lt;/h3&gt;
&lt;p&gt;It was a slog of interviews for the first half of the year. But I landed a position on a Developer Experience (DevX) team. It’s been great so far, I love working with an audience I care deeply about!&lt;/p&gt;
&lt;h3&gt;Created content ✅&lt;/h3&gt;
&lt;p&gt;Here are my top three most popular blog posts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/setting-up-rails-dev-containers/&quot; target=&quot;_blank&quot;&gt;Setting up Rails Dev Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/rails-stimulus-dynamic-nested-form/&quot; target=&quot;_blank&quot;&gt;Dynamic Nested Forms with Rails and Stimulus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jonathanyeong.com/writing-blog-posts-with-obsidian/&quot; target=&quot;_blank&quot;&gt;Writing blog posts with obsidian&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also started a newsletter, &lt;a href=&quot;https://dxdissected.com/&quot; target=&quot;_blank&quot;&gt;DX Dissected&lt;/a&gt;. It’s a forcing function to get deep into the DevX world. And even though I haven’t written as many newsletters as I wanted to, it’s a start.&lt;/p&gt;
&lt;p&gt;Finally, I released my first YouTube video in a long while! Snuck it in right before the year-end. The video is about using Dev containers to prototype without needing to worry about managing dependencies! &lt;a href=&quot;https://www.youtube.com/watch?v=6ILSqh0lnYk&quot; target=&quot;_blank&quot;&gt;Check it out&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Start earning money on the side ❌&lt;/h3&gt;
&lt;p&gt;Big fat X for this goal. I have some ideas, but I also have a mental blocker. What value am I providing? And can I provide that value consistently? I haven’t proven to myself that I can be consistent this year. So I’m focusing my energy on answering those questions first.&lt;/p&gt;
&lt;h2&gt;New hobbies&lt;/h2&gt;
&lt;p&gt;I started journaling my side hustle journey on pen and paper. It’s a nice way to slow down and process my thoughts. I’ve also noticed my handwriting becoming more readable over the entries.&lt;/p&gt;
&lt;p&gt;I also picked up pottery! I made a few pieces that are actually usable, which I’m happily surprised about. Here’s a pic of some pots I made.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1735224180/unsigned_obsidian_uploads/qoaqnxirojwkk8pxax2m.jpg&quot; alt=&quot;three pots I made, two bowls, and one cup. The cup holds liquid!&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;What’s to come in 2025&lt;/h2&gt;
&lt;p&gt;After this year, I have more clarity with what I want my life to look like. Communities gave me a lot of energy this year. And I’m starting to find my place on Bluesky as well. Which brings me to my 2025 theme:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building connection with people through communities. Both IRL and virtual. But also connection, in the form of building value for others. With this theme, I have some plans:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Invest more time building and taking part of communities (IRL and virtual)&lt;/li&gt;
&lt;li&gt;Consume more content for ideas (not only for entertainment)
&lt;ul&gt;
&lt;li&gt;Read technical books - 2 (baseline goal) to 4 (stretch goal).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Build and release production-grade apps - 1 (baseline goal) to 3 (stretch goal).&lt;/li&gt;
&lt;li&gt;Create more content (as much as possible)
&lt;ul&gt;
&lt;li&gt;I don’t know what amount is sustainable. I would like to create 12 videos and release 12 newsletters, but that seems high.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Pie in the sky goal: Speak at a conference!
&lt;ul&gt;
&lt;li&gt;Step one: figure out what to speak about.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Building Design Gut Instinct</title><link>https://jonathanyeong.com/writing/building-design-gut-instinct/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/building-design-gut-instinct/</guid><description>A guide to building your design gut instinct. And why having this instinct is useful.</description><pubDate>Sat, 07 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Ever looked at a website and thought “wow, that’s beautiful” but couldn’t explain why? That’s where design gut instinct comes in.&lt;/p&gt;
&lt;p&gt;This is a tip from &lt;a href=&quot;https://www.learnui.design/&quot; target=&quot;_blank&quot;&gt;Learn UI Design&lt;/a&gt;, a design course I’m taking. To be clear, I most definitely am not a UI design expert! There’s always something lacking in my own designs that I can’t quite put my finger on.&lt;/p&gt;
&lt;p&gt;Sure, I could use a UI library like Bootstrap to quickly create a good-looking site and save countless hours of learning what’s basically a new career. But sites that use these libraries have a template-y “smell”.&lt;/p&gt;
&lt;p&gt;I want to design rad sites that make someone go “ooohhh pretty”. And to do that, I’m working to improve my gut instinct.&lt;/p&gt;
&lt;p&gt;Here’s how you can do it too:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Find a website with a banging design. If you’re not sure where to find these sites, I’ll link to some resources at the end.&lt;/li&gt;
&lt;li&gt;Open up your tool of choice to store notes. I’ve found &lt;a href=&quot;https://www.figma.com/&quot; target=&quot;_blank&quot;&gt;Figma&lt;/a&gt; to be the best tool for this exercise. Here’s what my &lt;a href=&quot;https://www.figma.com/design/ouolRdogrOx6JenNooMNYu/Gut-Instinct-HW?node-id=0-1&amp;amp;t=orRB5SAlHFuXm6Ix-1&quot; target=&quot;_blank&quot;&gt;Gut Instinct Figma File&lt;/a&gt; looks like. You’re welcome to clone it, and modify!&lt;/li&gt;
&lt;li&gt;Write down what you liked about the website, take screenshots as well.&lt;/li&gt;
&lt;li&gt;Explain why you liked certain elements. Try to be as specific as you can. It’s okay to not know the terminology. But dig deeper than “cool font”.&lt;/li&gt;
&lt;li&gt;Observe elements that you might want to repeat in your own designs. And if there’s an element that, by changing, would make the design look worse. Note that down.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Having done this process for a handful of sites, I can tell I know more about design today than I did a week ago. Each analysis takes me about 10-15 minutes, and after a few sites I start seeing patterns emerge. And even though I don’t know all the design-y words, I write down what I feel when I see something I like.&lt;/p&gt;
&lt;p&gt;Let me share a concrete example.&lt;/p&gt;
&lt;p&gt;While looking at &lt;a href=&quot;https://fly.io/&quot; target=&quot;_blank&quot;&gt;Fly.io&lt;/a&gt;, I loved the header font. It was a serif font (Mackinac) that, for me, evoked a sense of trustworthiness but also whimsy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1733800000/unsigned_obsidian_uploads/ryyn5pdsh87ph2cmyrej.png&quot; alt=&quot;Fly.io header font: Mackinac. It says &amp;quot;A public cloud built for developers who ship&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now that I know about this font, I can use a tool like &lt;a href=&quot;https://www.typewolf.com/all-fonts&quot; target=&quot;_blank&quot;&gt;Typewolf&lt;/a&gt; to see what other sites are using it, and if they evoke the same emotion.&lt;/p&gt;
&lt;p&gt;Our goal is to build a library of patterns that we can draw from. We want to know what patterns work well, and why they work. The other benefit of documenting these patterns is to build an inspo board. When you start designing your own site, you’ll have a well of inspiration.&lt;/p&gt;
&lt;p&gt;If you’re wanting to build your design gut instinct, give the above steps a try and let me know what you come up with! You can tag me with your notes / Figma file on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;p&gt;Here’s a list of aggregators that share beautifully designed sites. Note: beautifully designed, sometimes not great UX.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://onepagelove.com/&quot; target=&quot;_blank&quot;&gt;One Page Love&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.siteinspire.com/&quot; target=&quot;_blank&quot;&gt;Site Inspire&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://godly.website/&quot; target=&quot;_blank&quot;&gt;Godly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.webdesign-inspiration.com/&quot; target=&quot;_blank&quot;&gt;WebDesign Inspiration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cssdesignawards.com/&quot; target=&quot;_blank&quot;&gt;CSS Design Awards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.awwwards.com/&quot; target=&quot;_blank&quot;&gt;Awwwards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://muz.li/blog/top-100-most-creative-and-unique-portfolio-websites-of-2024/&quot; target=&quot;_blank&quot;&gt;Muzli: Top 100 Most Creative and Unique Portfolio Websites of 2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To more easily figure out what font a site uses, I use &lt;a href=&quot;https://chromewebstore.google.com/detail/whatfont/jabopobgcpjmedljpbcaablpmlmfcogm?hl=en&quot; target=&quot;_blank&quot;&gt;What font browser extension&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>(Re)Building a reading habit</title><link>https://jonathanyeong.com/writing/rebuilding-reading-habit/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/rebuilding-reading-habit/</guid><description>Tips on how to build a reading habit: make reading a priority, figure out your reading vibe, prep your brain then check your brain, and finally curate your reading list.</description><pubDate>Fri, 06 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’m an atrocious reader of non-fiction articles or books. First off, I find myself constantly zoning out when reading a technical article. Secondly, I’m a sucker for instant gratification. I would rather clean the house for an hour then sit in a chair reading. For context, I do neither and watch YouTube instead. Those pottery videos aren’t going to watch themselves.&lt;/p&gt;
&lt;p&gt;In the past few weeks, I committed to reading more. To quote a toddler, “why”?&lt;/p&gt;
&lt;p&gt;Reading is a superpower. And it was one that I was missing out on. It unlocks new ideas, challenges preconceptions, and it helps me find my voice when writing. Also, by reading more I can finally complete the Goodreads reading challenge.&lt;/p&gt;
&lt;p&gt;Here’s what I’ve been doing to help me (re)build my reading habit.&lt;/p&gt;
&lt;h2&gt;Make reading a priority&lt;/h2&gt;
&lt;p&gt;The first step, and the only step that matters, is consistently making the time to read. Or more concretely, make reading a priority.&lt;/p&gt;
&lt;p&gt;When I have a busy day, reading used to get bumped into the “I don’t have time for this” bucket. No more! I now carve out 10 minutes, non-negotiable, reading time in the morning. I do this after I wake up, and after I make coffee. For those habit nerds, I make reading part of a habit chain.&lt;/p&gt;
&lt;p&gt;10 minutes doesn’t sound like much, but it’s more then what I was doing a month ago. And the goal is to slowly up this amount of time.&lt;/p&gt;
&lt;h2&gt;Figure out your reading vibe&lt;/h2&gt;
&lt;p&gt;Since 10 minutes isn’t a long time to read. I’ve found doing a vibe check at the start of a session a good way to get the most out of it. It sets myself up for a win and after the session I felt like I accomplished what I wanted.&lt;/p&gt;
&lt;p&gt;These are my reading vibes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reading to be entertained&lt;/li&gt;
&lt;li&gt;Reading to be informed&lt;/li&gt;
&lt;li&gt;Reading to understand&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Most of the time I want to be entertained or informed. If that’s what I’m feeling for the session, I’ll focus on those types of articles first. If there are more technical posts that seem interesting, I’ll save it for later.&lt;/p&gt;
&lt;p&gt;In the past, I’ve felt the pressure of taking notes, or always having to takeaway &lt;em&gt;something&lt;/em&gt; when reading. It doesn’t have to be this way. I’m becoming comfortable with reading an article, saying “huh, interesting”, and forgetting about it the next day. It’s a muscle, and as we read more, we’ll get better at digesting information.&lt;/p&gt;
&lt;p&gt;But there are days when I want to fully understand a concept. Or I have a technical article that I want to read. On these days, I normally spend a bit more time in my reading session. The next tip helps me digest information in these articles.&lt;/p&gt;
&lt;h2&gt;Prep your brain, then check your brain&lt;/h2&gt;
&lt;p&gt;Before diving into a deeply technical article. I prep my brain for learning. I’m basically telling my brain “hey dude, it’s not that bad, don’t zone out”.&lt;/p&gt;
&lt;p&gt;To prep, I skim over the article to get the general gist of what is happening before I start reading it. After skimming, I write down everything I know (or think I know) about the concept.&lt;/p&gt;
&lt;p&gt;Then after I’m done with the reading session, I write down at least &lt;strong&gt;one&lt;/strong&gt; thing that I learnt from the article. If I have time to write more, I’ll write about new concepts I found out, how they relate to my existing knowledge, or if they challenge what I currently know.&lt;/p&gt;
&lt;p&gt;I’ll rinse and repeat this process until I’ve finished with the article. The end result are notes I can look back on, and an article that I fully comprehend.&lt;/p&gt;
&lt;h2&gt;(Bonus) Curate your reading list&lt;/h2&gt;
&lt;p&gt;My final tip is to be selective about what you read. There’s so much content out there and not all content is the same. &lt;a href=&quot;https://dly.to/y69khDARDEa&quot; target=&quot;_blank&quot;&gt;Daily.dev &lt;/a&gt; is a good place to find new content. But it can get overwhelming if you don’t follow the tags or collections you care about.&lt;/p&gt;
&lt;p&gt;Nothing has beaten an RSS feed for getting new content. My RSS reader is my best friend. And it helps me keep up to date with the people I look up to most. If you’re curious, here’s my &lt;a href=&quot;https://raindrop.io/jonathanyeong/blogroll-50287458&quot; target=&quot;_blank&quot;&gt;blogroll&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My reading habit is still a work in progress, these tips have been helping, but I have a ways to go before I complete the Goodreads Challenge. I hope these tips have been helpful. Happy reading!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Articles I Enjoyed #1</title><link>https://jonathanyeong.com/writing/articles-i-enjoyed-1/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/articles-i-enjoyed-1/</guid><description>Sharing some articles that I&apos;ve enjoyed reading recently</description><pubDate>Thu, 05 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’m trying to read more these days, and I wanted to share some posts that I enjoyed recently. If there’s an article you think I should read or a blog I should follow, leave a comment or find me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;&lt;a href=&quot;https://brittanyellich.com/why-i-write/&quot; target=&quot;_blank&quot;&gt;Why I write&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://brittanyellich.com/&quot; target=&quot;_blank&quot;&gt;Brittany Ellich&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A lot of the reasons why Brittany writes resonate with me. Brittany’s also on a posting spree this month with the &lt;a href=&quot;https://bsky.app/profile/cassidoo.co/post/3lcbjdiahas2l&quot; target=&quot;_blank&quot;&gt;#blogvent challenge&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I write because it helps me to connect dots between the different things that I am reading, watching, and learning. Taking a topic and trying to form coherent sentences so that other folks can understand your thinking around it is a great way to synthesize your ideas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;https://www.taniarascia.com/redesign-version-7/&quot; target=&quot;_blank&quot;&gt;Redesign: Version 7.0: Sidebars, light-dark, and Bluesky&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.taniarascia.com/&quot; target=&quot;_blank&quot;&gt;Tania Rascia&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I’m a huge fan of redesign posts. Reading about the thought process behind design decisions is fascinating. Go check out Tania’s redesign if you haven’t. It’s really well done!&lt;/p&gt;
&lt;p&gt;It also led me to another of Tania’s posts &lt;a href=&quot;https://www.taniarascia.com/digital-gardening/&quot; target=&quot;_blank&quot;&gt;Tending to My Digital Garden&lt;/a&gt; that comes back to why I write, even if I forget my why sometimes.:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are a lot of reasons contributing to less activity from me, but I think one of the major ones is that I’ve been treating all my blog posts like they have to be precious. They’re not posts, I’ll think, they’re articles.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s weird that I feel weird about that, considering it’s my blog, so what else could or should it consist of aside from whatever I want to put on it? … it’s just whatever I find interesting and whatever I’m working on&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;So I’m writing this post to give myself permission to put whatever I want on the site.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;&lt;a href=&quot;https://jvns.ca/blog/2024/11/29/why-pipes-get-stuck-buffering/&quot; target=&quot;_blank&quot;&gt;Why pipes sometimes get “stuck”: buffering&lt;/a&gt;&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://jvns.ca/&quot; target=&quot;_blank&quot;&gt;Julia Evans&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;I never thought I needed to know about why pipes get stuck. But it’s happened to me before, and now I know why! Julia’s posts are the perfect example of I had a problem, I figured out why it was a problem, and now I’ll share it with everyone.&lt;/p&gt;
&lt;p&gt;TIL how grep decides to buffer its output&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here’s how &lt;code&gt;grep&lt;/code&gt; (and many other programs) decides to buffer its output:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check if stdout is a terminal or not using the &lt;code&gt;isatty&lt;/code&gt; function
&lt;ul&gt;
&lt;li&gt;If it’s a terminal, use line buffering (print every line immediately as soon as you have it)&lt;/li&gt;
&lt;li&gt;Otherwise, use “block buffering” – only print data if you have at least 8KB or so of data to print&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Personal Productivity Handbook</title><link>https://jonathanyeong.com/writing/personal-productivity-handbook/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/personal-productivity-handbook/</guid><description>My guide to personal productivity, including a lesson to not prematurely optimise, as well as an explanation of my tools and systems.</description><pubDate>Tue, 27 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Lately, I’ve been feeling extra energy to pursue my goals. If my motivation were a season, it would be summer right now. Sun is shining. High energy. Happy days. From experience, this energy doesn’t always stay, and I want to make the most of it now. In this post, I want to share a lesson I learnt about productivity and motivation. As well as walk through my current productivity system.&lt;/p&gt;
&lt;p&gt;I’ve realised what drains my energy most is decision fatigue. Especially, decisions on &lt;em&gt;how&lt;/em&gt; to be more productive. It sounds great on paper. If I can organise my &lt;em&gt;systems and workflows&lt;/em&gt;, then I can 10x the amount of work I output.&lt;/p&gt;
&lt;p&gt;However, if I spend all my energy organising, how much energy do I have left to create? If I create nothing, then what will 10x the amount of work equal? This lesson seems so obvious in retrospect, but I’ve fallen into the trap of working on my productivity efficiency over doing the actual work.&lt;/p&gt;
&lt;p&gt;So here’s a message for future me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Don’t prematurely optimise your productivity&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You don’t need to time block every minute, streamline your note-taking workflow, or invent a new way to track your projects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Focus on putting in the work, above all else.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Spending time optimising productivity feels good. It feels like I’m making progress towards goals. But after all the decision-making, I end up with a system that takes too much effort to maintain. And eventually the energy to keep the system up dissipates along with my motivation to do the work.&lt;/p&gt;
&lt;p&gt;I want the productivity system I have to be dead simple. I need it to do these things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tell me what I should do now.&lt;/li&gt;
&lt;li&gt;Capture thoughts wherever I am.&lt;/li&gt;
&lt;li&gt;Help me focus.&lt;/li&gt;
&lt;li&gt;Give me a way to process my thoughts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m going to share what my system is below. But even as I’m writing it out, I realise there are things that I can improve! It’s taking me a lot of self-control to not change it, but what I have written down is something that I don’t want to change for a while.&lt;/p&gt;
&lt;h2&gt;Productivity Tools &amp;amp; System&lt;/h2&gt;
&lt;p&gt;Here are the tools I use and how they fit into my productivity system.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1724762426/unsigned_obsidian_uploads/xa1rvda97wmaysurg9vx.png&quot; alt=&quot;My productivity system and tools&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Things (Apple only)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://culturedcode.com/things/&quot; target=&quot;_blank&quot;&gt;Things Website&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My to-do app of choice. Every morning, I’ll spend 5 minutes going over any tasks in my “Anytime” section and move things I need to do to “Today”. There’s a handy shortcut &lt;code&gt;cmd + t&lt;/code&gt; that will move things without me needing to drag and drop. Any tasks that come up during the day, I’ll note in my inbox and forget about them until the next day. Tasks that I don’t complete roll over to the next day. I try to keep my projects sparse, so I don’t spend too much time deciding where a task should go.&lt;/p&gt;
&lt;p&gt;The killer feature I’ve been using is &lt;a href=&quot;https://culturedcode.com/things/support/articles/2693493/&quot; target=&quot;_blank&quot;&gt;Templates projects&lt;/a&gt;. From a template, you can use Apple Shortcuts/automation to have a recurring project. It’s how to set up my weekly reset and review.&lt;/p&gt;
&lt;h3&gt;Logseq (cross-platform)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://logseq.com/&quot; target=&quot;_blank&quot;&gt;Logseq website&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Logseq is where I capture thoughts. Thoughts that come into my head naturally fit a bullet point outliner format that Logseq provides. I don’t spend too much time categorising my thoughts. I just dump everything into today’s Daily Note. Being able to capture thoughts and learning is more important to me than organisation (for now). I also have my Logseq vault saved on iCloud, which means I can access Logseq on the go.&lt;/p&gt;
&lt;h3&gt;Obsidian (cross-platform)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot;&gt;Obsidian website&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I published a post about &lt;a href=&quot;https://jonathanyeong.com/writing-blog-posts-with-obsidian/&quot; target=&quot;_blank&quot;&gt;writing blog posts with Obsidian&lt;/a&gt;. It’s my tool of choice when I want to synthesise thoughts.&lt;/p&gt;
&lt;h3&gt;Physical Journal (most recent addition)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.leuchtturm1917.ca/notebook-classic.html&quot; target=&quot;_blank&quot;&gt;Leuchtturm1917 ruled A5&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Lately, I’ve taken up writing in a physical journal. I’ve been documenting my entrepreneur / multi-hyphenate journey. I write in the mornings, usually for 25 minutes. Although my handwriting is atrocious, I’ve found it helpful to get a thought out of my head and into the real world. Especially around self-doubt. As I write, I’m able to process that thought more deeply. And reading back over what I wrote, I can see it in a different light. I’m also forcing myself to write neatly, which slows me down. It makes me process my thoughts, and not just word vomit.&lt;/p&gt;
&lt;h3&gt;Pomodoro timer (Flow.app)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://www.flow.app/&quot; target=&quot;_blank&quot;&gt;Flow.app website&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;My final tool is a Pomodoro timer. I use Flow.app, which is free. But there are many other options out there. I stick with the standard 25-minute block with a 5-minute break. A pomodoro has been the best option I found (so far) to give me a short feedback loop. I’ve started measuring project progress in terms of pomodoros. It tells me that I’ve been putting the work in. And tells me when I’ve probably spent &lt;em&gt;too&lt;/em&gt; long on a project.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I’ve been using this system for a couple of months now, and it has largely gotten out of my way in terms of creating. I don’t need to think about this system. Most things have a place, and while it might not be the most efficient. It allows me to get thoughts out of my head and into the world. I want to stick with this system until I see a real problem. Maybe I’m taking so many notes that I need to organise it. But that’s a good problem to have, and something I’m not facing right now.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Setting up dev containers in Rails</title><link>https://jonathanyeong.com/writing/setting-up-rails-dev-containers/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/setting-up-rails-dev-containers/</guid><description>Development containers are an alternative to local development. You can have a dev ready environment running in minutes. Here&apos;s how we can set one up in Rails, issues I ran into along the way, and my initial thoughts.</description><pubDate>Sun, 30 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://containers.dev/&quot; target=&quot;_blank&quot;&gt;Development containers&lt;/a&gt;, aka dev containers, have been around since 2019. Made by Microsoft, they allow you to use a container as a development environment. If you’ve used GitHub Codespaces, you’ve loaded a dev container. In Rails 7.2, there will be a new &lt;code&gt;devcontainer&lt;/code&gt; flag that will generate a dev container folder for you. Ever since I found out about Rails devcontainers at a local Ruby meetup, I’ve been eager to try them. If you also want to be an early adopter, here are a few issues I ran into.&lt;/p&gt;
&lt;h2&gt;Installing libraries&lt;/h2&gt;
&lt;p&gt;Before I can get dev containers working locally, I need to install a few libraries and extensions.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&quot; target=&quot;_blank&quot;&gt;VSCode dev container extension&lt;/a&gt;, which will run my application as a dev container inside VSCode.&lt;/p&gt;
&lt;p&gt;A docker runtime, that will run my devcontainer. If you’re on a personal machine, &lt;a href=&quot;https://orbstack.dev/&quot; target=&quot;_blank&quot;&gt;Orbstack&lt;/a&gt; (which I use) or &lt;a href=&quot;https://www.docker.com/products/docker-desktop/&quot; target=&quot;_blank&quot;&gt;Docker Desktop&lt;/a&gt; is a simple way to get started. Or if you’re on a work machine, &lt;a href=&quot;https://github.com/abiosoft/colima&quot; target=&quot;_blank&quot;&gt;Colima&lt;/a&gt; is another option.&lt;/p&gt;
&lt;p&gt;If you were to use Colima, you’ll have to follow these steps:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;brew install colima&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Docker to run my containers.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;brew install docker&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Docker compose, which is needed to run the dev container &lt;code&gt;compose.yaml&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;brew install docker-compose&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I also updated my &lt;code&gt;~/.docker/config.json&lt;/code&gt; to add the plugin&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;cliPluginsExtraDirs&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;/opt/homebrew/lib/docker/cli-plugins&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Setting up a Rails dev container&lt;/h2&gt;
&lt;p&gt;If you have Rails 7.2+ installed, you can create create a new Rails app with a devcontainer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;rails&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; dev-containers-app&lt;/span&gt;&lt;span&gt; --devcontainer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After opening the project in VSCode, you should get a popup asking if you want to &lt;code&gt;Reopen in container&lt;/code&gt;. If you don’t, you can use command palette (&lt;code&gt;cmd+shift+p&lt;/code&gt; for mac) and type &lt;code&gt;dev container: Reopen in Container&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you have a Rails 7.2+ project created and you want to add devcontainers&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;rails&lt;/span&gt;&lt;span&gt; devcontainer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which creates the devcontainer files for you:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1725126469/unsigned_obsidian_uploads/pdf1bfzoym0gi0xme5pu.png&quot; alt=&quot;output of running rails devcontainer. It figures out what libraries you use and creates the devcontainer folder and files for you.&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Initial thoughts&lt;/h2&gt;
&lt;p&gt;Here’s what I loved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Modifying files in the container also modified the files on my local machine. And vice versa! It’s a fast development workflow&lt;/li&gt;
&lt;li&gt;Git just works! It pulled my gitconfig from my machine, and I could push verified commits from the container.&lt;/li&gt;
&lt;li&gt;Since the app runs in a container, I don’t need to worry about dependency management any more. If I need a new dependency, I can pull it into my docker image.&lt;/li&gt;
&lt;li&gt;It works out of the box with VSCode dev container extension.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here’s what I didn’t like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can’t run &lt;code&gt;--skip-bundle&lt;/code&gt; and generate a &lt;code&gt;.devcontainer&lt;/code&gt; folder. Maybe someone can correct me if I’m wrong. I would like to not have to install anything locally if possible!&lt;/li&gt;
&lt;li&gt;The best dev experience is with VSCode. If you don’t use that editor, your workflow would be more involved. You would want to use the &lt;a href=&quot;https://github.com/devcontainers/cli&quot; target=&quot;_blank&quot;&gt;devcontainers/cli&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;If you have an underpowered machine, dev containers might not be for you. If your container run time isn’t beefy enough with RAM &amp;amp; CPU a-plenty, then your dev container might be sluggish.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jonathanyeong/rails-dev-containers&quot; target=&quot;_blank&quot;&gt;GitHub repo&lt;/a&gt; for reference.&lt;/p&gt;
&lt;h2&gt;Troubleshooting&lt;/h2&gt;
&lt;h3&gt;Unable to connect to DB through a DB management tool&lt;/h3&gt;
&lt;p&gt;I’m seeing an issue with Rails 8 and the default devcontainer setup. When I try to connect to my devcontainer DB using &lt;a href=&quot;https://sequel-ace.com/&quot; target=&quot;_blank&quot;&gt;Sequel Ace&lt;/a&gt; or &lt;a href=&quot;https://dbeaver.io/&quot; target=&quot;_blank&quot;&gt;DBeaver&lt;/a&gt;. Then I get a timeout error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Unable to connect to host localhost, or the request timed out.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Be sure that the address is correct and that you have the necessary privileges, or try increasing the connection timeout (currently 10 seconds).&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;MySQL said: Can&apos;t connect to local MySQL server through socket &apos;/tmp/mysql.sock&apos; (2)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To fix this issue remove &lt;code&gt;3306&lt;/code&gt; from &lt;code&gt;forwardPorts&lt;/code&gt; section in devcontainer.json. And add in &lt;code&gt;compose.yaml&lt;/code&gt; add this line under the &lt;code&gt;mysql&lt;/code&gt; block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ports:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  - &apos;3306:3306&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the &lt;code&gt;mysql&lt;/code&gt; block should now look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;mysql:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  image: mysql/mysql-server:8.0&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  restart: unless-stopped&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  environment:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    MYSQL_ALLOW_EMPTY_PASSWORD: &apos;true&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	MYSQL_ROOT_HOST: &quot;%&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  volumes:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  - mysql-data:/var/lib/mysql&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  networks:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  - default&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ports:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	- &apos;3306:3306&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rebuild your devcontainer and you should be able to connect to your DB using sequel ace or DBeaver.&lt;/p&gt;
&lt;h3&gt;Can’t connect to local server through socket&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; It looks like with the latest Rails release, these issues have been fixed.
When I first ran the dev container, the &lt;code&gt;bin/setup&lt;/code&gt; step errored:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;ActiveRecord::ConnectionNotEstablished: Can&apos;t connect to local server through socket &apos;/tmp/mysql.sock&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The issue is that &lt;code&gt;config/database.yaml&lt;/code&gt; is trying to read a &lt;code&gt;mysql.sock&lt;/code&gt; file that doesn’t exist in the container. The easiest fix is to connect to the DB through the network. Modify your &lt;code&gt;config/database.yaml&lt;/code&gt; file with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;amp;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  adapter&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mysql2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  encoding&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; utf8mb4&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pool&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;) { 5 } %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  username&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; root&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  password&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  host&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mysql&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  port&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 3306&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After this change, I was able to get my dev container up and running. However, when I tried to do Git things like &lt;code&gt;git status&lt;/code&gt;, I got an error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;fatal:&lt;/span&gt;&lt;span&gt; detected&lt;/span&gt;&lt;span&gt; dubious&lt;/span&gt;&lt;span&gt; ownership&lt;/span&gt;&lt;span&gt; in&lt;/span&gt;&lt;span&gt; repository&lt;/span&gt;&lt;span&gt; at&lt;/span&gt;&lt;span&gt; &apos;/workspaces/dev-containers-app&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;To&lt;/span&gt;&lt;span&gt; add&lt;/span&gt;&lt;span&gt; an&lt;/span&gt;&lt;span&gt; exception&lt;/span&gt;&lt;span&gt; for&lt;/span&gt;&lt;span&gt; this&lt;/span&gt;&lt;span&gt; directory,&lt;/span&gt;&lt;span&gt; call:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    git&lt;/span&gt;&lt;span&gt; config&lt;/span&gt;&lt;span&gt; --global&lt;/span&gt;&lt;span&gt; --add&lt;/span&gt;&lt;span&gt; safe.directory&lt;/span&gt;&lt;span&gt; /workspaces/dev-containers-app&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To resolve this issue, I updated the &lt;code&gt;devcontainer.json&lt;/code&gt; file. Adding a &lt;code&gt;postStartCommand&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	... &lt;/span&gt;&lt;span&gt;// Rest of config&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	&quot;&lt;/span&gt;&lt;span&gt;postStartCommand&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;span&gt;git config --global --add safe.directory ${containerWorkspaceFolder}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>30 Days of Posts Retrospective</title><link>https://jonathanyeong.com/writing/30-days-of-posts-retrospective/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/30-days-of-posts-retrospective/</guid><description>It&apos;s the end of my 30 days of post challenge and I wanted to write a retrospective</description><pubDate>Sun, 23 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today marks the end of my &lt;a href=&quot;https://jonathanyeong.com/30-days-of-posts/&quot; target=&quot;_blank&quot;&gt;30 days of posts challenge&lt;/a&gt;. Phew, it’s been a journey. I did miss two days of posts. But I was also able to get back to posting after I had broken my streak. I honestly thought I would run out of things to say after the first week. So I’m happy that I made it through the 30 days.&lt;/p&gt;
&lt;p&gt;What did I enjoy about the challenge?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The little voice in my head that tells me my posts are dumb was drowned out because of the time constraints. I wonder if that voice will be louder now.&lt;/li&gt;
&lt;li&gt;Having my blog become a playground of ideas and exploration was really fun. No idea was too small to write about. And making mistakes was okay!&lt;/li&gt;
&lt;li&gt;Finding ways to optimise my posting workflow. I doubled down on Obsidian, introduced some GitHub actions, and found faster ways to write.&lt;/li&gt;
&lt;li&gt;I also loved not “marketing” my posts. I didn’t post on social media, and I didn’t think about SEO at all. I wanted to optimise writing rather than views.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some struggles during the challenge:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Writing quality posts was difficult. I often ran out of time to edit. I didn’t feel like I was becoming a better writer. But a faster writer.&lt;/li&gt;
&lt;li&gt;There were some topics I couldn’t explore because they would take too much time.&lt;/li&gt;
&lt;li&gt;Coming back after breaking my streak was so hard. I was close to giving up at that point.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What’s next? I want to move to a more chill posting cadence. Maybe once or twice a week. And there’s some other content ideas that I intend to explore. I’m also going to dabble more into marketing my posts, so I can see what resonates with people. Overall, it was a fun challenge. I now know I have ideas to write about!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Reframing Imposter Syndrome</title><link>https://jonathanyeong.com/writing/reframing-imposter-syndrome/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/reframing-imposter-syndrome/</guid><description>I had a bad bout of imposter syndrome a few days ago. I talked to a coach who helped reframe my anxiety.</description><pubDate>Fri, 21 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I had a bad bout of imposter syndrome last week. I felt like I didn’t belong in my role, and that I was a fraud. Starting the day was anxiety inducing. I was expecting to be told that I was doing a terrible job. I needed help from someone. Thankfully, my company offers a few free coaching sessions, and I went to my first session this week. Here’s how my coach helped reframe my anxiety.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not all anxiety is bad.&lt;/strong&gt; There’s a bell curve, like with stress. Too little anxiety, you’re bored or coasting. Too much anxiety and you can’t function. Having just enough anxiety lights a fire in your belly. This feeling of anxiety was kicking my ass into gear. It was pushing me to learn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Think of the opposite scenarios.&lt;/strong&gt; I talked about not feeling like I knew enough to be called a staff. But my coach posed this question, what if you knew everything? Bored, was the answer I gave her. If I knew everything, I’d go looking for a new challenge. Talking about the opposite scenarios helped get my head out of the negative story I was telling myself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What did the company see in me?&lt;/strong&gt; This question was asked by my coach in the session. She went on to ask about my experiences when I first joined a company. What was my learning curve like? These questions helped me see that I had some valuable experience to bring to my role. It might not be technical skills, at least not yet, but I had non-tangible skills to bring to the table.&lt;/p&gt;
&lt;p&gt;Working on imposter syndrome is a work in progress. And reframing my anxiety this way doesn’t magically make it disappear. But now I have these questions in the back of my mind when I start to think I’m a fraud. I hope they help you.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Dynamic Nested Forms with Rails and Stimulus</title><link>https://jonathanyeong.com/writing/rails-stimulus-dynamic-nested-form/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/rails-stimulus-dynamic-nested-form/</guid><description>A guide on dynamically adding nested form elements using Stimulus.js and Rails. We&apos;ll cover the html and JS changes needed, as well as common gotchas.</description><pubDate>Wed, 19 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been away from Ruby on Rails for a &lt;a href=&quot;https://jonathanyeong.com/what-i-missed-about-ruby/&quot; target=&quot;_blank&quot;&gt;couple of years&lt;/a&gt;. And in that time, Rails introduced &lt;a href=&quot;https://hotwired.dev/&quot; target=&quot;_blank&quot;&gt;Hotwire&lt;/a&gt;, and &lt;a href=&quot;https://stimulus.hotwired.dev/&quot; target=&quot;_blank&quot;&gt;Stimulus&lt;/a&gt;. In this post, I extend my &lt;a href=&quot;https://jonathanyeong.com/what-i-missed-about-ruby/&quot; target=&quot;_blank&quot;&gt;Rails nested from tutorial&lt;/a&gt; by adding a button to dynamically add new nested form elements using a &lt;a href=&quot;https://www.stimulus-components.com/docs/stimulus-rails-nested-form/&quot; target=&quot;_blank&quot;&gt;Stimulus Nested Form component&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We’re going all in on Rails, so I’ll be using the default &lt;a href=&quot;https://github.com/rails/importmap-rails&quot; target=&quot;_blank&quot;&gt;importmap JS package manager&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Set up&lt;/h2&gt;
&lt;p&gt;Let’s first install the Stimulus nested form component&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;bin/importmap&lt;/span&gt;&lt;span&gt; pin&lt;/span&gt;&lt;span&gt; @stimulus-components/rails-nested-form&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update &lt;code&gt;app/javascript/controllers/application.js&lt;/code&gt; to register the stimulus component.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { Application } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &quot;@hotwired/stimulus&quot;&lt;/span&gt;&lt;span&gt; // Should exist already&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; RailsNestedForm &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;@stimulus-components/rails-nested-form&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; application &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Application&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;start&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;// Should exist already&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;application&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;register&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;nested-form&apos;&lt;/span&gt;&lt;span&gt;, RailsNestedForm)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s modify our &lt;a href=&quot;https://jonathanyeong.com/nested-forms-in-rails/&quot; target=&quot;_blank&quot;&gt;previous form&lt;/a&gt; by moving our nested fields into a partial. First create the partial &lt;code&gt;app/views/training_sessions/_training_step_fields.html.erb&lt;/code&gt; and add the following code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# app/views/training_sessions/_training_step_fields.html.erb&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt;&lt;span&gt; f&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;label&lt;/span&gt;&lt;span&gt; :description&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;%= f.text_field :description %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After we create our partial, we can modify the form in &lt;code&gt;new.html.erb&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# app/views/training_sessions/new.html.erb&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt;&lt;span&gt; form_with &lt;/span&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt;: @training_session &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;span&gt; |form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  Session Steps:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%= form.fields_for :training_steps do |training_steps_form| %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%=&lt;/span&gt;&lt;span&gt; render &lt;/span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;training_step_fields&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;locals&lt;/span&gt;&lt;span&gt;: { &lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;: training_steps_form } &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt; &quot;Create New Session&quot;&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, when we go to &lt;code&gt;training_sessions/new&lt;/code&gt; we should see the same behaviour as before.&lt;/p&gt;
&lt;h2&gt;Implementing Stimulus Nested Form Component&lt;/h2&gt;
&lt;p&gt;Let’s implement the Stimulus component. Modify your &lt;code&gt;new.html.erb&lt;/code&gt; to add a Stimulus target:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt;&lt;span&gt; form_with &lt;/span&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt;: @training_session &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;span&gt; |form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  Session Steps:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;template data-nested-form-target=&quot;template&quot;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fields_for&lt;/span&gt;&lt;span&gt; :training_steps&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TrainingStep&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;child_index&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;NEW_RECORD&apos;&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;span&gt; |training_steps_form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= render partial: &quot;training_step_fields&quot;, locals: { f: training_steps_form }%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%&lt;/span&gt;&lt;span&gt; end&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;/template&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fields_for&lt;/span&gt;&lt;span&gt; :training_steps&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;span&gt; |training_steps_form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= render partial: &quot;training_step_fields&quot;, locals: { f: training_steps_form } %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%&lt;/span&gt;&lt;span&gt; end&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;div data-nested-form-target=&quot;target&quot;&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;button type&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;button&quot;&lt;/span&gt;&lt;span&gt; data&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;nested-form#add&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;Add&lt;/span&gt;&lt;span&gt; Training&lt;/span&gt;&lt;span&gt; Step&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;button&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt; &quot;Create New Session&quot;&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if we reload our app there’s now a button that lets you add more training steps 🎉.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1718756584/unsigned_obsidian_uploads/iekjw7427bkqf58y8kaf.gif&quot; alt=&quot;Add nested form field demo using Stimulus Nested Form component&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Breaking down the changes&lt;/h2&gt;
&lt;p&gt;Let’s break down the changes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;template data&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;nested&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;template&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;template&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;div data&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;nested&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;form&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;target&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;target&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;div&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We register two targets with Stimulus, &lt;code&gt;template&lt;/code&gt; and &lt;code&gt;target&lt;/code&gt;. The &lt;code&gt;template&lt;/code&gt; target, which in Stimulus, gets referred to as &lt;code&gt;this.templateTarget&lt;/code&gt; is inserted into &lt;code&gt;this.targetTarget&lt;/code&gt; via &lt;code&gt;insertAdjacentHTML&lt;/code&gt;. The template is defined similar to our original definition of the training_step fields. But it has two additional params: &lt;code&gt;TrainingSteps.new&lt;/code&gt; and &lt;code&gt;child_index: &apos;NEW_RECORD&apos;&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fields_for&lt;/span&gt;&lt;span&gt; :training_steps&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;TrainingStep&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;child_index&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&apos;NEW_RECORD&apos;&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;span&gt; |training_steps_form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rails requires the index of nested fields to be unique. It enforces uniqueness via a &lt;code&gt;child_index&lt;/code&gt;. See the &lt;a href=&quot;https://apidock.com/rails/ActionView/Helpers/FormHelper/fields_for#512-Setting-child-index-while-using-nested-attributes-mass-assignment&quot; target=&quot;_blank&quot;&gt;Rails docs&lt;/a&gt;. We must keep the &lt;code&gt;NEW_RECORD&lt;/code&gt; value. Stimulus nested form component will replace the term &lt;code&gt;NEW_RECORD&lt;/code&gt; with &lt;code&gt;Date.getTime().toString()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TrainingStep.new&lt;/code&gt; is used to build a new instance of the Training Step model. Without this model, we’ll see some odd behaviour where we add two fields every time we press the “Add Training Step” button. Note: the &lt;strong&gt;two&lt;/strong&gt; comes from our controller: &lt;code&gt;2.times { @training_session.training_steps.build }&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fun fact&lt;/strong&gt;: even though two form elements get added, only one element get saved. Can you guess why? That’s right! It’s because the child index for those elements are the same. Rails will only save the last of the two values.&lt;/p&gt;
&lt;p&gt;For reference, here’s &lt;a href=&quot;https://github.com/stimulus-components/stimulus-rails-nested-form/blob/master/src/index.ts&quot; target=&quot;_blank&quot;&gt;the component source code&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;At the time of writing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rails 7.1&lt;/li&gt;
&lt;li&gt;Ruby 3.3.2&lt;/li&gt;
&lt;li&gt;Stimulus Nested Form Component 5.0.0&lt;/li&gt;
&lt;li&gt;Stimulus 3.2.2&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Infrastructure 101: What is a server?</title><link>https://jonathanyeong.com/writing/infra-101-what-is-a-server/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/infra-101-what-is-a-server/</guid><description>Beginning my infrastructure journey from the very basics. Let&apos;s talk about servers.</description><pubDate>Tue, 18 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You’ve been working hard on a Rails application. It’s living on your local machine, but now you’re ready to show the world. What’s next?&lt;/p&gt;
&lt;p&gt;We need a server! A piece of hardware which can download your Rails app, run it, and allow people to access it. There are many kinds of servers. &lt;em&gt;When I was doing research, the term “server” was losing all meaning.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cloud Servers
&lt;ul&gt;
&lt;li&gt;A virtual server hosted on a cloud computing environment like AWS, GCP, Azure etc. Examples include, AWS EC2 or Digital Ocean Droplets.&lt;/li&gt;
&lt;li&gt;Server is managed by the provider.&lt;/li&gt;
&lt;li&gt;Pricing is on demand, can be dynamically scaled up or down.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Virtual Private Servers (VPS)
&lt;ul&gt;
&lt;li&gt;A virtual server that runs on top of a physical server. The physical server may have multiple virtual servers running on it. It can be hosted on on-premises physical servers or within data centres.&lt;/li&gt;
&lt;li&gt;Server managed by you.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On-premise (on-prem) Servers
&lt;ul&gt;
&lt;li&gt;Physical machines in a data centre (or even a dedicated machine at home) that will run your application.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We’ll focus on cloud servers, since that’s what most hobby projects would use. You can spin up a cloud server through the provider UI, or through an Infrastructure as Code tool like Terraform. Once the server is up and running, the next steps will be to load your application. I’ll leave that for the next post!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;P.S. In most cases, especially if you’re a solo dev trying to get something running quickly, I’d recommend &lt;a href=&quot;https://fly.io/&quot; target=&quot;_blank&quot;&gt;Fly.io&lt;/a&gt;. Fly.io is an infrastructure as a service (IaaS) platform. It has a simple deploy process, &lt;code&gt;fly launch &amp;amp;&amp;amp; fly deploy&lt;/code&gt;, and is cheaper than Heroku. Here’s the &lt;a href=&quot;https://fly.io/docs/rails/getting-started/&quot; target=&quot;_blank&quot;&gt;guide to getting Rails up and running with Fly.io&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>How to Effectively Pair Program</title><link>https://jonathanyeong.com/writing/pairing-to-solve-problems/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/pairing-to-solve-problems/</guid><description>Pair programming is a powerful way to solve problems. It might not be for everyone, but here are some ways to make it more effective.</description><pubDate>Mon, 17 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For me, pair programming is the best way to solve complex problems. I’ve had my fair share of poor pairing sessions. Ones where I was zoning out, or falling asleep, and that left me drained at the end of the session. But I’ve also felt the power of a good pair programming session. I could bounce ideas back and forth with my partner. I felt confident about the feature we released. And I learnt something new. Pairing leads to safer code, and reduces the knowledge silos on a team. The positives outweigh the negatives.&lt;/p&gt;
&lt;p&gt;Pairing might not be for everyone, but I wanted to share some tips that have helped me in my pairing sessions. Hopefully, it’ll help make your next pairing session more pleasant.&lt;/p&gt;
&lt;h2&gt;Come with a topic to pair on - the problem matters&lt;/h2&gt;
&lt;p&gt;It’s important to have the right topic when pairing. Here’s what you &lt;strong&gt;shouldn’t&lt;/strong&gt; pair on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tasks that require a lot of research up front.&lt;/li&gt;
&lt;li&gt;Topics where one person doesn’t know the domain at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pairing sessions where both participants are reading documentation is no fun. It’s much better to have read documentation beforehand and come with ideas. And if one of the participants doesn’t know the domain at all, it’s much better to spend that time context sharing. When pairing, you want both participants contributing to the conversation.&lt;/p&gt;
&lt;p&gt;Everything else is fair game.&lt;/p&gt;
&lt;h2&gt;Know when to stop&lt;/h2&gt;
&lt;p&gt;Both participants should discuss when to stop or to take breaks ahead of time. They don’t need to be set in stone. If it’s a complex issue, I’d suggest taking the entire day to pair. The agreed times can change throughout the session. Maybe at the start, you take breaks every one or one and a half hours. But then later, you take breaks for longer and more frequently.&lt;/p&gt;
&lt;h2&gt;Use the right tools&lt;/h2&gt;
&lt;p&gt;This point is specifically about remote pairing. Find a tool that lets you interact with your partners screen. Both with a pointer, and also the ability to type. It’s a much better pairing experience when you can jump in to type out a missing command. Instead of saying it aloud or sending it over in a message. Here are my favourite tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tuple.app/&quot; target=&quot;_blank&quot;&gt;Tuple&lt;/a&gt; - high definition, low latency, you can draw on the screen, and typing on your partner’s screen is seamless.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://code.visualstudio.com/learn/collaboration/live-share&quot; target=&quot;_blank&quot;&gt;VSCode Live Share&lt;/a&gt; - Live share is a VSCode plugin that allows participants to join and see your VSCode session. They retain their editor settings, which is a neat feature. You can have people follow you, and they can type in your window. I would use this plugin with video conferencing, or even Tuple.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Don’t get locked into the driver &amp;amp; navigator dynamic&lt;/h2&gt;
&lt;p&gt;There’s a “best practice” when pairing to define two roles clearly: a driver and a navigator. The driver is doing all the typing. While the navigator is offering suggestions, catching errors, and guiding the driver. Participants each get a turn driving and navigating.&lt;/p&gt;
&lt;p&gt;In my experience, these roles aren’t so clearly defined. As a driver, you’re still navigating. And as a navigator, you could step in to drive at any point. The point is to make pairing interactive and engaging. You want there to be a feedback loop for both participants. Don’t get hung up on who’s driving and who’s navigating. Make sure both participants get a chance to type.&lt;/p&gt;
&lt;h2&gt;Check in at the end of the session&lt;/h2&gt;
&lt;p&gt;At the end of pairing, check in with your partner. How did they feel about the session? What did we learn? What should we do next? It doesn’t need to be super formal, just a conversation. But it closes the loop and sets you up for future pairing sessions.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Moving Slow to Move Fast</title><link>https://jonathanyeong.com/writing/moving-slow-to-move-fast/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/moving-slow-to-move-fast/</guid><description>Kelsey Hightower dropping knowledge bombs that I absolutely resonate with</description><pubDate>Fri, 14 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ll always be a &lt;a href=&quot;https://twitter.com/kelseyhightower&quot; target=&quot;_blank&quot;&gt;Kelsey Hightower&lt;/a&gt; fan. He was on a recent Changelog podcast “&lt;a href=&quot;https://changelog.com/podcast/595&quot; target=&quot;_blank&quot;&gt;Retired not Tired&lt;/a&gt;” and he said a lot of things that resonated. One piece of knowledge he dropped was around these electrical installer competitions. He talks about how the person whose winning is methodical.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;He moving slow…and he’s done 30% faster then everybody else. He is methodical.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Contrast it to the people who are moving fast:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Everybody else is picking it up, moving the ladder, picking it up, moving the ladder&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;They continue to talk about moving slow to move fast which I agree with. Most of my screw ups happen because I’m moving fast. It feels like I’m moving the ladder repeatedly. Except instead of a ladder it’s push a PR, find a bug, push a PR.&lt;/p&gt;
&lt;p&gt;Then Kelsey adds the missing piece. That to be methodical, you have to be good at what you do. You have to put in the work.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It’s the people that don’t quite have the skills down and they have to compensate for the lack of skills by doing stuff seven times&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The entire podcast episode was full of great stuff. Give it a listen if you haven’t already!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Terraform magic</title><link>https://jonathanyeong.com/writing/terraform-magic/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/terraform-magic/</guid><description>I&apos;m learning about Terraform and it&apos;s all magic to me</description><pubDate>Thu, 13 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’m learning about &lt;a href=&quot;https://developer.hashicorp.com/terraform&quot; target=&quot;_blank&quot;&gt;Terraform&lt;/a&gt;, and it’s all magic to me right now. I know that Terraform is an Infrastructure as Code (IaC) tool. The alternative to IaC is clicking around GUIs to create your infrastructure. I’m not going to lie, the AWS UI is overwhelming. Also, it’s basically all acronyms, and &lt;a href=&quot;https://www.inc.com/jeff-steen/why-you-should-stop-using-acronyms-right-now.html&quot; target=&quot;_blank&quot;&gt;I hate acronyms&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I fully support Infrastructure as Code (IaC). It’s safe, consistent, and repeatable. You can version your infrastructure, share it, and reuse it. Now back to Terraform. How does it work?&lt;/p&gt;
&lt;p&gt;Here’s my best guess with the little research I’ve done so far.&lt;/p&gt;
&lt;p&gt;Terraform has a provider registry that you can reference in your &lt;code&gt;tf&lt;/code&gt; file. A provider is a plugin for a specific infrastructure service, a service like AWS or Digital Ocean, for example. When you run &lt;code&gt;terraform apply&lt;/code&gt;, the tool will read your configuration and call some APIs in the provider, which in turn hook into APIs on the infrastructure service that will create / modify / delete the infrastructure. Your Terraform configuration goes into &lt;code&gt;main.tf&lt;/code&gt;, which is a naming convention, or it can go into any other &lt;code&gt;*.tf&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;The next step is to use Terraform to set up some infrastructure for a Rails application. I’m planning on going through the &lt;a href=&quot;https://developer.hashicorp.com/terraform/intro/v1.1.x/core-workflow&quot; target=&quot;_blank&quot;&gt;Core Terraform Workflow&lt;/a&gt; for guidance.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Beginning my Developer Experience journey</title><link>https://jonathanyeong.com/writing/developer-experience-journey/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/developer-experience-journey/</guid><description>In this post, I share my definition of Developer experience, how we measure it, and what we measure, as well as a note on survey building</description><pubDate>Wed, 12 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For once, I’m excited about my job. I’m working on the Developer Experience (DevEx) team. I get to help other developers for a living! One of my first tasks is to understand the vibes with our current developer experience through a survey. But before I could start the survey, I had to learn some things.&lt;/p&gt;
&lt;h2&gt;What is good DevEx&lt;/h2&gt;
&lt;p&gt;I know a good developer experience when I see it. But I’ve never defined it. Here’s my stab:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Good DevEx reduces friction on a developer’s daily tasks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It can look like better tooling, CI/CD, build time improvements, apps to help with pairing etc. A good developer experience makes for a productive developer.&lt;/p&gt;
&lt;h2&gt;How do we measure DevEx&lt;/h2&gt;
&lt;p&gt;We can measure DevEx both qualitatively and quantitatively. It’s important to have both. For example, we can pull objective data about build time. However, that account for any flaky test reruns, or merge queue times. It’s easier and faster to get a measure of DevEx through qualitative data (i.e. surveys).&lt;/p&gt;
&lt;h2&gt;What do we measure?&lt;/h2&gt;
&lt;p&gt;The two metric frameworks I found were &lt;a href=&quot;https://docs.gitlab.com/ee/user/analytics/dora_metrics.html&quot; target=&quot;_blank&quot;&gt;DORA&lt;/a&gt; (DevOps Research and Assessment) and &lt;a href=&quot;https://queue.acm.org/detail.cfm?id=3454124&quot; target=&quot;_blank&quot;&gt;SPACE&lt;/a&gt;. SPACE stands for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Satisfaction and well-being&lt;/li&gt;
&lt;li&gt;Performance&lt;/li&gt;
&lt;li&gt;Activity&lt;/li&gt;
&lt;li&gt;Communication and collaboration&lt;/li&gt;
&lt;li&gt;Efficiency and flow&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DORA is focused on DevOps measures like deployment frequency, lead time for changes, change failure rate, and time to restore services. SPACE is all encompassing. DORA can be considered an instance of SPACE.&lt;/p&gt;
&lt;p&gt;From a recent &lt;a href=&quot;https://getdx.com/podcast/dora-space-devex-choosing-framework/&quot; target=&quot;_blank&quot;&gt;Engineering Enablement, podcast episode&lt;/a&gt;,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“DORA presents at least a starting point… I see DORA metrics having been really successful is for organizations that are focused on DevOps transformation or who are going from batched releases to continuous delivery”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For my DevEx measures, I’d rather not focus on DORA. There were other functions at my company that had these metrics. Instead, I wanted to answer these set of questions, inspired by SPACE.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the engineering onboarding experience like? Can we reduce errors with setup?&lt;/li&gt;
&lt;li&gt;What does collaboration look like to you? Is it pairing, mentorship, both, or none?&lt;/li&gt;
&lt;li&gt;How do you test your changes? Are you confident that what goes into production will work as expected?&lt;/li&gt;
&lt;li&gt;How long does it take for a change to get pushed to production? How long is the feedback loop for you?&lt;/li&gt;
&lt;li&gt;What is the experience working in a monolith? Have you started committing changes to other teams domains?&lt;/li&gt;
&lt;li&gt;What tooling are you missing? What prevents you from answering positively to the above questions?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Building the survey&lt;/h2&gt;
&lt;p&gt;The survey is segmented out by team, function (front end or back end), tenure, and level. I expanded the questions I shared above and gave them options based on the &lt;a href=&quot;https://en.wikipedia.org/wiki/Likert_scale&quot; target=&quot;_blank&quot;&gt;Likert Scale&lt;/a&gt;. Here’s an example:&lt;/p&gt;
&lt;p&gt;I have the tools necessary to get my work done
&lt;em&gt;1 - strongly disagree, 2 - disagree, 3 - neither agree nor disagree, 4 - agree, 5 - strongly agree&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Something I didn’t realise about surveys was how hard they are to write. I want the result of the survey to help answer the questions I have in mind, that will be actionable, but also not be biased due to leading questions. I found &lt;a href=&quot;https://support.cultureamp.com/en/articles/7048658-guide-to-designing-a-survey&quot; target=&quot;_blank&quot;&gt;CultureAmp’s Guide to Designing a Survey&lt;/a&gt; a great resource.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I’m only beginning my DevEx journey, so there’s a lot to learn. If you have any resources, please share! Here’s a list of all the resources in this post and more:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.blog/2023-06-08-developer-experience-what-is-it-and-why-should-you-care/&quot; target=&quot;_blank&quot;&gt;Github DevEx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://microsoft.github.io/code-with-engineering-playbook/developer-experience/&quot; target=&quot;_blank&quot;&gt;Microsoft DevEx&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://getdx.com/podcast/&quot; target=&quot;_blank&quot;&gt;Engineering Enablement Podcast&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gitlab.com/ee/user/analytics/dora_metrics.html&quot; target=&quot;_blank&quot;&gt;Gitlab DORA metrics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://queue.acm.org/detail.cfm?id=3454124&quot; target=&quot;_blank&quot;&gt;The SPACE of developer productivity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Likert_scale&quot; target=&quot;_blank&quot;&gt;Likert Scale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.cultureamp.com/en/articles/7048658-guide-to-designing-a-survey&quot; target=&quot;_blank&quot;&gt;CultureAmp’s Guide to Designing a Survey&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Tools I use daily as a solo dev</title><link>https://jonathanyeong.com/writing/tools-i-use-daily-solo-dev/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/tools-i-use-daily-solo-dev/</guid><description>All the tools that I use on a daily basis as a solo dev</description><pubDate>Tue, 11 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was inspired by &lt;a href=&quot;https://dev.to/nickytonline/tools-that-keep-me-productive-1no5&quot; target=&quot;_blank&quot;&gt;Nick Taylor’s post “Tools that keep me productive”&lt;/a&gt;, to document tools I use daily. These tools help me build and showcase projects, stay organised, and keep me productive. Most of these tools are cross-platform, at least between macOS and Linux.&lt;/p&gt;
&lt;h2&gt;Editor + Terminal&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://code.visualstudio.com/&quot; target=&quot;_blank&quot;&gt;VSCode&lt;/a&gt; is my main editor. I’ve been playing around with Zed, but it doesn’t have the plugin ecosystem (yet) for me to switch over. I’m using the &lt;a href=&quot;https://draculatheme.com/visual-studio-code&quot; target=&quot;_blank&quot;&gt;Dracula&lt;/a&gt; theme, and &lt;a href=&quot;https://github.com/Twixes/SF-Mono-Powerline&quot; target=&quot;_blank&quot;&gt;SF Mono Powerline font&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Bonus tip: if you’re livestreaming, you can enable screencast mode in VSCode to have it show keystrokes. If you want to turn off all keystrokes, go to settings, search for &lt;code&gt;keyboardOptions&lt;/code&gt;, and uncheck &lt;code&gt;show raw keys&lt;/code&gt;. After that setting is turned off, it will only display modifier keys.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://iterm2.com/&quot; target=&quot;_blank&quot;&gt;iTerm2&lt;/a&gt; is my terminal of choice. I use &lt;code&gt;zsh&lt;/code&gt; as my shell, with &lt;a href=&quot;https://ohmyz.sh/&quot; target=&quot;_blank&quot;&gt;oh-my-zsh&lt;/a&gt; installed to manage plugins for &lt;code&gt;zsh&lt;/code&gt;. &lt;a href=&quot;https://starship.rs/&quot; target=&quot;_blank&quot;&gt;Starship&lt;/a&gt; is my prompt, it shows valuable information on a project out of the box. Finally, I added &lt;a href=&quot;https://github.com/rupa/z&quot; target=&quot;_blank&quot;&gt;z&lt;/a&gt; to jump around to different folders from anywhere.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1718052393/unsigned_obsidian_uploads/ok17eceaxcovljnbyevx.gif&quot; alt=&quot;using z to jump around to a folder anywhere&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://asdf-vm.com/&quot; target=&quot;_blank&quot;&gt;asdf&lt;/a&gt; is my plugin manager. It can manage versions of multiple different languages.&lt;/p&gt;
&lt;h2&gt;Screen capturing&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://cleanshot.com/&quot; target=&quot;_blank&quot;&gt;CleanShotX&lt;/a&gt; (paid) is what I use to capture screenshots, videos, and GIFs. It’s a new tool in my workflow, so I haven’t had a chance to fully use it yet.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;QuickTime player was what I was using to screen record before CleanShotX. It’s a free tool that comes with macOS. The screen recording feature displays mouse clicks and allows you to select a window or area to record.&lt;/p&gt;
&lt;h2&gt;Design&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.figma.com/&quot; target=&quot;_blank&quot;&gt;Figma&lt;/a&gt; is my go-to design tool. It has a free plan that gives you a limited number of projects and pages. It also has powerful features like auto-layout, and components which make building designs faster. The only downside is that you have to pay monthly for multiple projects.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://penpot.app/&quot; target=&quot;_blank&quot;&gt;Penpot&lt;/a&gt; is a free, open-source Figma alternative that I’ve been playing around with. I don’t have the same muscle memory as Figma, and it’s browser only. But it seems like a promising free alternative.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://excalidraw.com/&quot; target=&quot;_blank&quot;&gt;Excalidraw&lt;/a&gt; is my whiteboarding / brainstorming tool of choice. I love Excalidraw. I have a whole &lt;a href=&quot;https://jonathanyeong.com/tools-i-use-excalidraw/&quot; target=&quot;_blank&quot;&gt;post&lt;/a&gt; about it! Figma is a too heavy for rough wireframing or brainstorming. That’s where Excalidraw shines.&lt;/p&gt;
&lt;h2&gt;Note-Taking&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://logseq.com/&quot; target=&quot;_blank&quot;&gt;Logseq&lt;/a&gt; for learning new concepts, or taking quick notes. I dump all my knowledge into Logseq and let references between those notes form over time. There’s no real concept of folder structures, which means I don’t need to spend a lot of time figuring out where notes should go. It also comes with a built-in &lt;a href=&quot;https://docs.logseq.com/#/page/queries&quot; target=&quot;_blank&quot;&gt;query language&lt;/a&gt;, which can help you find information. My favourite part about Logseq is its unlinked references. Meaning you don’t need to explicitly add references to things, Logseq can do it for you.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot;&gt;Obsidian&lt;/a&gt; for long form writing, like blog posts. I have a &lt;a href=&quot;https://jonathanyeong.com/writing-blog-posts-with-obsidian/&quot; target=&quot;_blank&quot;&gt;blog post workflow in Obsidian&lt;/a&gt;. It’s also where I write journal entries or do my goal setting. Both Obsidian and Logseq store notes locally, so I can write offline.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;https://www.notion.so/&quot; target=&quot;_blank&quot;&gt;Notion&lt;/a&gt; for documentation that needs some kind of UI (like a table of contents), that I won’t need if I’m offline. It’s where my brag doc lives.&lt;/p&gt;
&lt;h2&gt;Project management&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/issues/planning-and-tracking-with-projects&quot; target=&quot;_blank&quot;&gt;GitHub Projects&lt;/a&gt; is how I manage all my projects. I’ve set up a single GitHub Project that pulls in issues from various GitHub repos. It gives me an overview of all the things that I want to do, and then lets me drill down into what I should do next.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Slow productivity framework</title><link>https://jonathanyeong.com/writing/slow-productivity-framework/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/slow-productivity-framework/</guid><description>Sharing my journey from toxic productivity to embracing the Slow Productivity framework</description><pubDate>Mon, 10 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Years ago, I was fully hooked to the world of productivity. At the time, I was getting up at the crack of dawn to get as much done. I used time blocking to try to maximise every hour of my day. Working a full-time job, then spending 4–5 hours outside the job working on various side projects. I would cut out any activity that I deemed “unproductive”. And when something unexpected came along that interrupted my time block, it caused anxiety.&lt;/p&gt;
&lt;p&gt;Only recently did I realise that this productivity mindset was toxic. Yes, it took me a pandemic and also burning out to realise that toxic productivity exists. Gaslighting myself is my forte. To this day, I’m still battling with the feeling of not doing enough. That there aren’t enough hours in the day to be “productive”.&lt;/p&gt;
&lt;p&gt;To have a healthier mindset, I needed a new “productivity” framework. I wanted a framework that would help me prioritise what to do outside my 9-5 job. Something that would let me roll with the punches when the unexpected came up. And would help me feel good about what I chose to spend time on. So I present to you my &lt;strong&gt;“slow productivity” framework&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When I’m trying to decide what to do. I have to answer three questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Will I have fun?&lt;/li&gt;
&lt;li&gt;Will I learn something?&lt;/li&gt;
&lt;li&gt;Will I regret it if I didn’t do it?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If I say yes to any one of these questions, I should do the thing and not feel guilty about doing it. Some activities are more worth doing than others, I can tell if it’s a yes to multiple questions. But if it’s a no to all questions, then I won’t do the activity.&lt;/p&gt;
&lt;p&gt;Here’s an example of how I applied the framework this morning.&lt;/p&gt;
&lt;p&gt;I woke up feeling refreshed, which rarely happens. I wanted to immediately start working on a side project before work. It’s something that I would have fun with and learn something. Ticking two of the three questions. This task is something that I would consider productive.&lt;/p&gt;
&lt;p&gt;Now I sit at my desk, turn on my laptop, and get ready to code. But my cat jumps up on my table and wanted pets. She was far too cute, and demanding, to ignore. There went my productive morning time. In the past, I would’ve been annoyed and felt guilty that I was doing something unproductive. But today, I ran through the three questions. Am I having fun? Yes. Will I regret it if I didn’t do it? 100% yes. Always pet the cat if it offers.&lt;/p&gt;
&lt;p&gt;Sometimes unexpected events happen that interrupt your productive task. With this framework, I feel less anxiety and guilt around these events. It might not work for everyone, but I hope it helps you feel better about doing the “unproductive” activities.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Nested forms in Rails</title><link>https://jonathanyeong.com/writing/nested-forms-in-rails/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/nested-forms-in-rails/</guid><description>Quick tutorial on how to build nested forms in Rails</description><pubDate>Sun, 09 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;What I’m trying to achieve&lt;/h2&gt;
&lt;p&gt;I have two models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Training Steps&lt;/li&gt;
&lt;li&gt;Training Session which has many Training Steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a user creates a Training Session, I want a form that contains fields for Training Steps inside the Training Session form. When I submit the form, I will create a Training Session object along with the related Training Steps.&lt;/p&gt;
&lt;h2&gt;1. Accepting nested attributes&lt;/h2&gt;
&lt;p&gt;First, I added &lt;code&gt;accepts_nested_attributes_for&lt;/code&gt; to my Training Session model.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; TrainingStep&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ApplicationRecord&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  belongs_to &lt;/span&gt;&lt;span&gt;:training_session&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; TrainingSession&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ApplicationRecord&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  has_many &lt;/span&gt;&lt;span&gt;:training_steps&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  accepts_nested_attributes_for &lt;/span&gt;&lt;span&gt;:training_steps&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;reject_if&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;lambda&lt;/span&gt;&lt;span&gt; { |attributes| attributes[&lt;/span&gt;&lt;span&gt;&apos;description&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;blank?&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, nested attribute updating is turned off. With &lt;code&gt;accepts_nested_attributes_for&lt;/code&gt; I’m able to create a Training Session and the Training Steps in one go. With this method, &lt;code&gt;TrainingSession&lt;/code&gt; will now accept a key &lt;code&gt;training_steps_attributes&lt;/code&gt; as a value. With &lt;code&gt;reject_if&lt;/code&gt;,  it will also silently ignore any records that don’t pass the lambda.&lt;/p&gt;
&lt;h2&gt;2. Adding the controller &lt;code&gt;new&lt;/code&gt; method&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; TrainingSessionsController&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ApplicationController&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  def&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    @training_session &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; TrainingSession&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;times&lt;/span&gt;&lt;span&gt; { @training_session&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;training_steps&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In my &lt;code&gt;new&lt;/code&gt; method, I’ll initialise a new Training Session model assigned to &lt;code&gt;@training_session&lt;/code&gt;, and build two &lt;code&gt;training_steps&lt;/code&gt; models along with it. &lt;code&gt;@training_session&lt;/code&gt; will be used in the view.&lt;/p&gt;
&lt;h2&gt;3. Adding a nested form in the view&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;%=&lt;/span&gt;&lt;span&gt; form_with &lt;/span&gt;&lt;span&gt;model&lt;/span&gt;&lt;span&gt;: @training_session &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;span&gt; |form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  Session Steps:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;ul&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;fields_for&lt;/span&gt;&lt;span&gt; :training_steps&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;span&gt; |training_steps_form| &lt;/span&gt;&lt;span&gt;%&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;li&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;%=&lt;/span&gt;&lt;span&gt; training_steps_form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;label&lt;/span&gt;&lt;span&gt; :description&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        &amp;lt;%= training_steps_form.text_field :description %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;/&lt;/span&gt;&lt;span&gt;li&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%&lt;/span&gt;&lt;span&gt; end&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;/ul&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;%=&lt;/span&gt;&lt;span&gt; form&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;submit&lt;/span&gt;&lt;span&gt; &quot;Create New Session&quot;&lt;/span&gt;&lt;span&gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the view, &lt;code&gt;new.html.erb&lt;/code&gt;, we’re using &lt;code&gt;form_with&lt;/code&gt; to build a form that routes to the correct &lt;code&gt;create&lt;/code&gt; path. Rails will infer the correct routes based on the resource, in this case &lt;code&gt;@training_session&lt;/code&gt;. &lt;code&gt;fields_for&lt;/code&gt; is then used to refer to the nested &lt;code&gt;training_steps&lt;/code&gt; objects. This method is used to refer to a specific model object, &lt;code&gt;training_steps&lt;/code&gt; in this case, but it yields to the &lt;code&gt;form_with&lt;/code&gt; element. In other words, you would use &lt;code&gt;fields_for&lt;/code&gt; inside a &lt;code&gt;form_with&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;TIL - &lt;code&gt;form_with&lt;/code&gt; is the new way to build forms. I used &lt;code&gt;form_for&lt;/code&gt; in the past but that’s now obselete (&lt;a href=&quot;https://github.com/rails/rails/pull/26976&quot; target=&quot;_blank&quot;&gt;here’s the PR with the change to form_with&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;4. Creating a nested object&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; TrainingSessionsController&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ApplicationController&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  def&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    @training_session &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; TrainingSession&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    2&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;times&lt;/span&gt;&lt;span&gt; { @training_session&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;training_steps&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;build&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  def&lt;/span&gt;&lt;span&gt; create&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    training_session &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; TrainingSession&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt;(training_session_params)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    if&lt;/span&gt;&lt;span&gt; training_session&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      training_session&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;save&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      redirect_to training_sessions_path&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    else&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      render &lt;/span&gt;&lt;span&gt;&quot;new&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  private&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  def&lt;/span&gt;&lt;span&gt; training_session_params&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    params&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;:training_session&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;permit&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;training_steps_attributes&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;:id&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;:description&lt;/span&gt;&lt;span&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the controller, we can add the &lt;code&gt;create&lt;/code&gt; method to handle creating the Training Session object. Notice that we use the &lt;code&gt;training_steps_attributes&lt;/code&gt; in &lt;code&gt;training_session_params&lt;/code&gt;. We’ll pass these parameters directly into &lt;code&gt;TrainingSession.new&lt;/code&gt; and when we &lt;code&gt;save&lt;/code&gt; Rails will create both the TrainingSession object and the associated Training Steps objects.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Tina CMS: a retrospective</title><link>https://jonathanyeong.com/writing/tina-cms-a-retrospective/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/tina-cms-a-retrospective/</guid><description>A retrospective on trying to implement Tina CMS into my workflow</description><pubDate>Sat, 08 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I decided to try &lt;a href=&quot;https://tina.io/&quot; target=&quot;_blank&quot;&gt;TinaCMS&lt;/a&gt; (aka Tina). It’s another headless CMS that supports Git. Perfect for my workflow! It has built in media management, it can validate metadata fields on a post, and it can push directly to your Git repo. Unfortunately, it didn’t fit with my flow, as much as I wanted it to. Here are the challenges I faced, and why I ultimately decided not to use Tina.&lt;/p&gt;
&lt;h2&gt;Separate content repo woes&lt;/h2&gt;
&lt;p&gt;This blog consists of two repos; a website repo, and a content repo. The content repo is a flat folder with all my blog posts in Markdown. Luckily, TinaCMS works with a &lt;a href=&quot;https://tina.io/guides/tinacms/separate-content-repo/guide/&quot; target=&quot;_blank&quot;&gt;separate content repo&lt;/a&gt;. When you set up Tina, you add a &lt;code&gt;tina&lt;/code&gt; folder that contains configuration for Tina cloud to read. This configuration is where you specify the metadata of your blog posts, the path to your content, among a few other settings.&lt;/p&gt;
&lt;p&gt;Something that the docs don’t mention is the requirement to have a &lt;code&gt;tina&lt;/code&gt; folder in both the website repo, and the content repo. When you run &lt;code&gt;tinacms build&lt;/code&gt; it will generate some files in your &lt;code&gt;tina&lt;/code&gt; folder in the content repo. Unfortunately, this folder does not play nicely with AstroJS if you’re using &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/&quot; target=&quot;_blank&quot;&gt;content collections&lt;/a&gt;. Including this folder caused the following error:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1717901753/unsigned_obsidian_uploads/tnet5xinqxvhowxgmzxt.png&quot; alt=&quot;AstroJS complaining about mixing content and data entries because the Tina config folder lived in my content repo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I was able to get around this issue by removing the folder during my Netlify build step. But it was major pain point. With a hacky solution.&lt;/p&gt;
&lt;h2&gt;Failing build on deploy previews&lt;/h2&gt;
&lt;p&gt;I didn’t realise at the time, but out of the box, Tina doesn’t support multiple branches. It comes as a &lt;a href=&quot;https://tina.io/docs/tina-cloud/branching/&quot; target=&quot;_blank&quot;&gt;feature for the paid editorial workflow&lt;/a&gt;. It wasn’t a dealbreaker, but it was an inconvenience. Anytime I wanted to push a new feature, I couldn’t test it out in a deploy preview. I would have to test in production.&lt;/p&gt;
&lt;h2&gt;Flat file structure issues&lt;/h2&gt;
&lt;p&gt;After the separate content repo woes, and failed deploy previews, I was finally able to get Tina up and running. I could see, and edit my posts! But then I faced another blocker. I couldn’t create posts. Whenever I pressed save, I would get the following error:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1717901821/unsigned_obsidian_uploads/jmvokcplie5lazy1amwx.png&quot; alt=&quot;Problem creating new documents in Tina cloud. It was due to a forward slash in front of my blog post slug&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The reason was because I set my &lt;code&gt;path&lt;/code&gt; in Tina config to an empty string. I needed to set it as an empty string, so I could display my posts in the flat folder. However, Tina will add a &lt;code&gt;/&lt;/code&gt; when saving a post, which will cause the call to GitHub to fail.&lt;/p&gt;
&lt;p&gt;This blocker was the final nail in the coffin. &lt;a href=&quot;https://github.com/tinacms/tinacms/issues/4543&quot; target=&quot;_blank&quot;&gt;I opened an issue for it.&lt;/a&gt;. I’m hoping they’ll accept my suggestion, or come up with a better approach in the future!&lt;/p&gt;
&lt;h2&gt;Deciding not to use TinaCMS&lt;/h2&gt;
&lt;p&gt;The culmination of all the issues above, made me realise that Tina wasn’t for me. I really wanted to get TinaCMS to work. I had a few back and forth’s on Tina Discord in an attempt to resolve my issues. But last week, I decided to not continue with it. I doubled down on Obsidian as I started writing more. And I found the Obsidian Cloudinary plugin, which solved my media management problems. I had many learnings during this process. And I’m pretty confident saying, if you have an AstroJS site with a separate content repo, TinaCMS might not be for you.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Fixing missing Ruby versions in asdf</title><link>https://jonathanyeong.com/writing/til-missing-ruby-version-in-asdf/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/til-missing-ruby-version-in-asdf/</guid><description>TIL: how to fix missing Ruby versions in asdf</description><pubDate>Fri, 07 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I use &lt;a href=&quot;https://asdf-vm.com/&quot; target=&quot;_blank&quot;&gt;asdf&lt;/a&gt; to manage my Ruby versions. It’s a version manager that can manage multiple languages. Meaning I don’t need to install a separate Ruby version manager with a Node version manager, for example.&lt;/p&gt;
&lt;p&gt;I ran into an issue today where &lt;code&gt;asdf&lt;/code&gt; wasn’t showing the latest Ruby version. To fix it I ran:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;asdf&lt;/span&gt;&lt;span&gt; plugin&lt;/span&gt;&lt;span&gt; update&lt;/span&gt;&lt;span&gt; ruby&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then to install the latest ruby version, and set it as the default Ruby version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;asdf&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; ruby&lt;/span&gt;&lt;span&gt; latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;asdf&lt;/span&gt;&lt;span&gt; global&lt;/span&gt;&lt;span&gt; ruby&lt;/span&gt;&lt;span&gt; latest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use &lt;code&gt;.tool-versions&lt;/code&gt; to specify which Ruby version you want at the project level (see &lt;a href=&quot;https://asdf-vm.com/manage/configuration.html#tool-versions&quot; target=&quot;_blank&quot;&gt;asdf docs for more info&lt;/a&gt;).&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Advice for Effective Developers</title><link>https://jonathanyeong.com/writing/advice-for-effective-developers/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/advice-for-effective-developers/</guid><description>In this post, I share advice that has made me a better developer. I was inspired to write down my thoughts after a recent SyntaxFM podcast, 11 habits of highly effective developers.</description><pubDate>Thu, 06 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Syntax FM released a podcast episode &lt;a href=&quot;https://syntax.fm/show/778/11-habits-of-highly-effective-developers/transcript&quot; target=&quot;_blank&quot;&gt;11 habits of highly effective developers&lt;/a&gt; yesterday, it’s filled with anecdotes, and great advice. If you haven’t already, give it a listen! Scott Tolinksi and Wes Bos shared these 11 habits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understand stakeholders and business goals&lt;/li&gt;
&lt;li&gt;Have an open mind about new tech&lt;/li&gt;
&lt;li&gt;Help others with problems&lt;/li&gt;
&lt;li&gt;Understand work-life balance&lt;/li&gt;
&lt;li&gt;Pay attention to details&lt;/li&gt;
&lt;li&gt;Be curious and always learning&lt;/li&gt;
&lt;li&gt;Ask for help when needed&lt;/li&gt;
&lt;li&gt;Have fun with development&lt;/li&gt;
&lt;li&gt;Be empathetic to users and coworkers&lt;/li&gt;
&lt;li&gt;Be part of the developer community&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While listening, I made a mental note to keep practising these habits, I loved them! Especially, having fun with development. Usually, I see programming as a way to solve business problems. I forget that it can also be used to make fun things!&lt;/p&gt;
&lt;p&gt;After the podcast ended, I thought about what advice would I have given my past self to be a more effective developer? So I made a list! I’ll add to it over time, and I hope you’ll find the advice useful.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Naming is hard. Understanding the domain of your application can help. If possible, err on being specific with your name rather than generic.&lt;/p&gt;
&lt;p&gt;Build your pattern matching muscle. Not regex (although that can be useful), but patterns with solving problems. Watch how others solve problems, debug, and write code. Go through the same process yourself over and over again.&lt;/p&gt;
&lt;p&gt;Pair frequently and with different people. The benefits of pairing greatly outweigh working solo on complex issues.&lt;/p&gt;
&lt;p&gt;“For each desired change, make the change easy (warning: this may be hard), then make the easy change” - &lt;a href=&quot;https://x.com/KentBeck/status/250733358307500032&quot; target=&quot;_blank&quot;&gt;Kent Beck&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Find the most straightforward path to solve a problem. If this path doesn’t exist, can you make it happen? Only when you’ve exhausted your options should you look at more complicated solutions. &lt;em&gt;I find this advice the hardest to follow.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Introspect often and stay away from autopilot mode. After finishing a tutorial, take the time to understand what you’ve learnt and how you could use that knowledge in other ways.&lt;/p&gt;
&lt;p&gt;Pursue what is interesting to you. Drown out the hubbub, the trends, the absolutists. If you don’t know what fascinates you yet; experiment.&lt;/p&gt;
&lt;p&gt;Go broad on a topic before going deep. This advice ties in with the one above. Experiment until you find something, then go deep on it. Going broad diversifies your mental models. Going deep strengthens your models.&lt;/p&gt;
&lt;p&gt;Have a career document aka brag document. Use it to show your impact at your role. Save all the good stuff people say about you or things that you’re proud of. Look at it whenever you’re feeling down or feeling like an imposter.&lt;/p&gt;
&lt;p&gt;Practice saying no. Overcommitting is stressful. Burnout is real, take the time to care for yourself.&lt;/p&gt;
&lt;p&gt;You don’t need to write blog posts, produce content, or commit to open source to be a successful developer. But it can be a nice way to give back to the community.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Using Github Actions to fetch content</title><link>https://jonathanyeong.com/writing/using-github-actions-to-fetch-content/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/using-github-actions-to-fetch-content/</guid><description>My content lives in a private repository separated from the main website repository. It got annoying to manually keep changes in sync. Instead, I used Github Actions to automate publishing posts.</description><pubDate>Wed, 05 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I recently added more automation around my blog post publishing workflow. Previously, I shared how I use &lt;a href=&quot;https://jonathanyeong.com/writing-blog-posts-with-obsidian/&quot; target=&quot;_blank&quot;&gt;Obsidian to write blog posts&lt;/a&gt;. In this post, I want to show you what automation I’ve added to help publish a post.&lt;/p&gt;
&lt;p&gt;Before I dive in, I need to provide some context. This site is separated into two repos. A &lt;a href=&quot;https://github.com/jonathanyeong/personal-website&quot; target=&quot;_blank&quot;&gt;website repo&lt;/a&gt;, that’s where all the AstroJS goodness lives. And then a &lt;a href=&quot;https://github.com/jonathanyeong/personal-website-content&quot; target=&quot;_blank&quot;&gt;content repo&lt;/a&gt;, that’s where all my posts live. The content repo is a Git submodule inside the website repo. These two are separated for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I can keep my content commits separate from my website commits.&lt;/li&gt;
&lt;li&gt;I have dedicated version control over my content.&lt;/li&gt;
&lt;li&gt;I have visibility controls over my content. Meaning, I can set my content to provide if I ever wanted to.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unfortunately, with two separate repos, I have a few extra steps to publish. I need to push the new post to the content repo. Then pull in those changes to the website repo, before pushing again.&lt;/p&gt;
&lt;h2&gt;Automation introduced&lt;/h2&gt;
&lt;p&gt;I used two Github Action scripts to automate fetching the latest content changes, and pushing to main.&lt;/p&gt;
&lt;p&gt;In my content repo, I have a &lt;a href=&quot;https://github.com/jonathanyeong/personal-website-content/blob/main/.github/workflows/main.yml&quot; target=&quot;_blank&quot;&gt;script that runs whenever I push to main&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Trigger deploy&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  push&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  workflow_dispatch&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  build&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Trigger parent repository update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          curl -XPOST https://api.github.com/repos/jonathanyeong/personal-website/dispatches \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          -H &apos;Accept: application/vnd.github+json&apos; \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          -u jonathanyeong:${{ secrets.ACCESS_TOKEN }} \&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          --data &apos;{&quot;event_type&quot;: &quot;deploy content changes&quot;}&apos;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This script will dispatch an event to my website repository whenever there’s a push to main. The data field doesn’t matter, and could be whatever event type you want. GitHub has more docs on &lt;a href=&quot;https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#create-a-repository-dispatch-event&quot; target=&quot;_blank&quot;&gt;creating a repository dispatch event&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In my website repo, I have a &lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/.github/workflows/fetch-content-updates.yml&quot; target=&quot;_blank&quot;&gt;script that listens to dispatch events&lt;/a&gt; and runs when it receives one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;Fetch Content Updates&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;repository_dispatch&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;workflow_dispatch&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pull_content_updates&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; pull_content_updates&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Checkout repository&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; actions/checkout@v4&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          token&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ${{ secrets.ACCESS_TOKEN }}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          submodules&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Pull submodule&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git pull --recurse-submodules&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git submodule update --init --recursive --remote&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Push changes&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git config --global user.email &quot;hey@jonathanyeong.com&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git config --global user.name &quot;Jonathan Yeong&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git add .&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git commit -m &quot;[Workflow] Updating website content&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          git push&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;repository_dispatch&lt;/code&gt; handler will watch for dispatch events. When it gets one, it will pull down the latest changes from the submodule, and then push the updates to main. Here are the &lt;a href=&quot;https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch&quot; target=&quot;_blank&quot;&gt;GitHub docs on &lt;code&gt;repository_dispatch&lt;/code&gt;&lt;/a&gt;. After pushing to main branch on my website repo, Netlify will build my site and publish it.&lt;/p&gt;
&lt;p&gt;Note: I included &lt;code&gt;workflow_dispatch&lt;/code&gt; so I can manually trigger a content fetch, and push for testing.&lt;/p&gt;
&lt;h2&gt;What’s next?&lt;/h2&gt;
&lt;p&gt;These GitHub Actions were my first foray into automating my workflow. And I’d love to do more. It’d be great to have a linter that would catch any markdown issues before I pushed. I also want to automate pushing posts to the content repo. It would be cool if I had a “publish” button in Obsidian that would do a commit and push for me.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Scheduling Posts with AstroJS</title><link>https://jonathanyeong.com/writing/changelog-scheduling-posts-with-astrojs/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/changelog-scheduling-posts-with-astrojs/</guid><description>Quick demo on how I schedule posts with AstroJS</description><pubDate>Mon, 03 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Before going on a trip this weekend, I wrote multiple posts so I didn’t need to write on my trip. However, to keep my writing streak, I had to manually release posts by setting their draft property to &lt;code&gt;false&lt;/code&gt;. That sucked. Today I released scheduling posts so I don’t need to go through this pain again. Here’s how I did it.&lt;/p&gt;
&lt;h2&gt;Implementing scheduled posts&lt;/h2&gt;
&lt;p&gt;A scheduled post on my site is any post that is NOT in draft, with a &lt;code&gt;pubDate&lt;/code&gt; that is in the future. I want to display a scheduled post when the &lt;code&gt;pubDate&lt;/code&gt; is in the past. With the previous sentence in mind, I created a file that exported a method &lt;code&gt;hasPubDatePassed&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; type&lt;/span&gt;&lt;span&gt; { CollectionEntry } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;astro:content&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { formatInTimeZone } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;date-fns-tz&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (date&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; formatInTimeZone&lt;/span&gt;&lt;span&gt;(date, &lt;/span&gt;&lt;span&gt;&apos;Etc/UTC&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;yyyy-MM-dd&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; hasPubDatePassed&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (post&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	let&lt;/span&gt;&lt;span&gt; todaysDate &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	todaysDate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setHours&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt;(todaysDate) &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; default&lt;/span&gt;&lt;span&gt; hasPubDatePassed&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I used this method in my &lt;code&gt;filterPublishPosts&lt;/code&gt; helper (&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/utilities/filterPublishedPosts.ts&quot; target=&quot;_blank&quot;&gt;source&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; filterPublishedPosts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (allPosts&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;[])&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;[] &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; allPosts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sort&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    (a, b) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; b&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;valueOf&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt; a&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;valueOf&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  )&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;filter&lt;/span&gt;&lt;span&gt;((post) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; import&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;meta&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DEV &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; !&lt;/span&gt;&lt;span&gt;isDraftPost&lt;/span&gt;&lt;span&gt;(post) &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; hasPubDatePassed&lt;/span&gt;&lt;span&gt;(post));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where this &lt;code&gt;filterPublishedPosts&lt;/code&gt; method sorts the posts and then displays only those that are not in draft and who’s &lt;code&gt;pubDate&lt;/code&gt; is in the past.&lt;/p&gt;
&lt;p&gt;Note: &lt;code&gt;import.meta.env.DEV&lt;/code&gt; is used to display all posts in local development.&lt;/p&gt;
&lt;h3&gt;Timezone Gotcha’s&lt;/h3&gt;
&lt;p&gt;After a &lt;a href=&quot;https://jonathanyeong.com/fixing-blog-timezone-bug/&quot; target=&quot;_blank&quot;&gt;bug with timezones&lt;/a&gt; sometime last year, I knew it wasn’t enough to do a date comparison like below. &lt;code&gt;todaysDate&lt;/code&gt; would have a timestamp but &lt;code&gt;pubDate&lt;/code&gt; doesn’t have a timestamp. If we applied timezones to these two dates we could publish a post earlier then expected.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// ❌: 🕛 Timezone issues&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; hasPubDatePassed&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (post&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	let&lt;/span&gt;&lt;span&gt; todaysDate &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; todaysDate &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To avoid dealing with timezones, I set the time of today’s date to &lt;code&gt;00:00&lt;/code&gt;. This time matches the time of &lt;code&gt;pubDate&lt;/code&gt;. After setting the time, I convert both dates into UTC timezone, and output strings in a format like &lt;code&gt;2024-05-30&lt;/code&gt;. Finally, I do a string comparison that will return all posts with pubDate today or in the past.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// ✅: 🕛 Sets both times to UTC&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; hasPubDatePassed&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (post&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CollectionEntry&lt;/span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;&quot;blog&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	let&lt;/span&gt;&lt;span&gt; todaysDate &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	todaysDate&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;setHours&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;	return&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt;(todaysDate) &lt;/span&gt;&lt;span&gt;&amp;gt;=&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt;(post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pubDate);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;I have a scheduled post for tomorrow to test this feature 🤞 it all works. &lt;em&gt;Note to self: automated tests would be nice&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;Update: GitHub Action to trigger a build&lt;/h3&gt;
&lt;p&gt;After I finished writing this post, I realised I missed one big thing. When AstroJS builds, it’s building as a static site. Which means at the time of the build, only those posts will exist. So scheduling a post in the future will never work because it will never exist in the AstroJS build step 🤦🏻.&lt;/p&gt;
&lt;p&gt;To resolve this issue, I introduced a GitHub Action that would &lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/.github/workflows/trigger-scheduled-build.yml&quot; target=&quot;_blank&quot;&gt;trigger daily at 6am UTC&lt;/a&gt; (around 2am EST). The GitHub Action will call a &lt;a href=&quot;https://docs.netlify.com/configure-builds/build-hooks/&quot; target=&quot;_blank&quot;&gt;Netlify build hook&lt;/a&gt; , which will build the site and pull in the scheduled post. It’s important to keep the build hook a secret because it’s not authed, meaning anyone can &lt;code&gt;curl&lt;/code&gt; the URL and trigger a build.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;Scheduled Build&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  schedule&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    -&lt;/span&gt;&lt;span&gt; cron&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;0 6 * * *&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  trigger_netlify_build&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; trigger_netlify_build&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Call netlify build&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          curl -X POST -d {} ${{ secrets.NETLIFY_BUILD_HOOK }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Writing blog posts with Obsidian</title><link>https://jonathanyeong.com/writing/writing-blog-posts-with-obsidian/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/writing-blog-posts-with-obsidian/</guid><description>My end to end writing workflow for my blog using Obsidian as the main editor. From how I setup Obsidian for writing, image management, and automating git pushes.</description><pubDate>Sun, 02 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been writing a lot of Markdown lately. And I have two thoughts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I love writing Markdown&lt;/li&gt;
&lt;li&gt;I hate looking at Markdown&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When I write pure Markdown, I feel like it’s too noisy. I’ve tried some editors that mirror Markdown like &lt;a href=&quot;https://code.visualstudio.com/docs/languages/markdown#_markdown-preview&quot; target=&quot;_blank&quot;&gt;VSCode with the Markdown preview&lt;/a&gt;, or using an online Markdown editor like &lt;a href=&quot;https://stackedit.io/&quot; target=&quot;_blank&quot;&gt;StackEdit&lt;/a&gt;. I couldn’t deal with the split, my eyes didn’t know what to look at! There’s probably live Markdown previews online as well. But copy-and-pasting content from the browser to a file was too many steps.&lt;/p&gt;
&lt;p&gt;Then I found &lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot;&gt;Obsidian&lt;/a&gt;. It’s a writing app that’s popular in the PKM (personal knowledge management) space. You can use it to organise your thoughts and link together different concepts. It fits perfectly in my workflow because it provides a distraction-free environment, live preview, and page templates.&lt;/p&gt;
&lt;h2&gt;Setting up the blog post workflow&lt;/h2&gt;
&lt;p&gt;To set up the workflow, I needed to symlink two folders. One was to my content folder in my Astro project. And the other was to the folder I created for blog posts in Obsidian.&lt;/p&gt;
&lt;p&gt;Opening up my terminal, I first got the path to my content folder. Then &lt;code&gt;cd&lt;/code&gt; into my Obsidian folder. This folder was set up when I created a new “Vault” (see &lt;a href=&quot;https://help.obsidian.md/Files+and+folders/Manage+vaults&quot; target=&quot;_blank&quot;&gt;Obsidian Manage Vaults doc&lt;/a&gt; for more information). I then created a new folder, and symlinked the two.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;cd&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;path-to-obsidian-vaul&lt;/span&gt;&lt;span&gt;t&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;mkdir&lt;/span&gt;&lt;span&gt; BlogPosts&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;ln&lt;/span&gt;&lt;span&gt; -s&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;path-to-content-folde&lt;/span&gt;&lt;span&gt;r&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; BlogPosts&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, any new posts created in the &lt;code&gt;BlogPosts&lt;/code&gt; folder in Obsidian will also appear in my &lt;code&gt;path-to-content-folder&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Setting up a template&lt;/h2&gt;
&lt;p&gt;Obsidian has a built-in Template plugin that I use for all my posts. Learn more in the &lt;a href=&quot;https://help.obsidian.md/Plugins/Templates&quot; target=&quot;_blank&quot;&gt;Obsidian Template documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have a folder named &lt;code&gt;Templates&lt;/code&gt; that contains a file &lt;code&gt;blog-post-template&lt;/code&gt;. The template is common front matter I use for all my posts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {{&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {{&lt;/span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;template&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;A template&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;featured&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; false&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding images to posts&lt;/h2&gt;
&lt;p&gt;Initially, I added image by dropping the file into my Astro project than referencing the file in Obsidian. This is clunky for a couple of reasons.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My content is separate from my website. Meaning I have to sync git pushes to ensure my images aren’t breaking on publish.&lt;/li&gt;
&lt;li&gt;I have to jump back and forth between apps to get the image details.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A better alternative is to drop a link to an image. Cloudinary is a popular tool to manage media, and there’s a &lt;a href=&quot;https://jordanhandy.github.io/obsidian-cloudinary-uploader/&quot; target=&quot;_blank&quot;&gt;Cloudinary Uploader plugin&lt;/a&gt; that will upload your image to cloudinary and return a link in one step.&lt;/p&gt;
&lt;p&gt;You do need a Cloudinary account and an API key, but after some setup it’s been working flawlessly.&lt;/p&gt;
&lt;h2&gt;Pushing posts to Git&lt;/h2&gt;
&lt;p&gt;After writing a post, I would switch to my terminal and run a few commands to push the post to the Git repository. Running these commands became anonying quickly. So I built a &lt;a href=&quot;https://github.com/jonathanyeong/obsidian-publish-post&quot; target=&quot;_blank&quot;&gt;small Obsidian plugin&lt;/a&gt; that would run the git push for me!&lt;/p&gt;
&lt;h2&gt;What’s next?&lt;/h2&gt;
&lt;p&gt;I’ve tried using &lt;a href=&quot;https://jonathanyeong.com/tina-cms-a-retrospective/&quot; target=&quot;_blank&quot;&gt;Tina CMS but unfortunately it didn’t fit with my workflow&lt;/a&gt;. But moving to a CMS would be a logical next step. Media management is still a pain point. I also want to start building components to use in MDX files, and I know Obsidian won’t handle that out of the box.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Making a custom kebab case method for pretty URLs</title><link>https://jonathanyeong.com/writing/changing-to-kebabcase/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/changing-to-kebabcase/</guid><description>I was surprised that Rails and Ruby didn&apos;t have a built in kebab case method. Adding a gem seemed overkill for me. So I created a custom method to serve pretty URLs.</description><pubDate>Sat, 01 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Recently, I was working on a Rails app where I wanted to make some pretty URLs for it. I have a route where you can view a Dog resource &lt;code&gt;GET /dog/1&lt;/code&gt;. Having an ID in the URL is not what I want. People could increment the ID to potentially see other dogs. But more importantly, it’s not personal! I wanted the URL to show your dog name instead, like &lt;code&gt;/dog/albie&lt;/code&gt; or &lt;code&gt;/dog/st-george-bernard-the-3rd&lt;/code&gt;. I needed a way to make pretty kebab case URLs.&lt;/p&gt;
&lt;h2&gt;Pretty kebab case URLs&lt;/h2&gt;
&lt;p&gt;To get a URL that’s not the default &lt;code&gt;id&lt;/code&gt;, I added the &lt;a href=&quot;https://edgeapi.rubyonrails.org/classes/ActiveRecord/Integration/ClassMethods.html#method-i-to_param&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;to_param&lt;/code&gt; method&lt;/a&gt; to my model. Where &lt;code&gt;slug&lt;/code&gt; was a column on my Model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; to_param&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  slug&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since I wanted the slug to be auto-generated, I had to transform the &lt;code&gt;name&lt;/code&gt; column to kebab case (see below). Much to my surprise, there wasn’t a Ruby / Rails &lt;code&gt;kebabcase&lt;/code&gt; method:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; Dog&lt;/span&gt;&lt;span&gt; &amp;lt; &lt;/span&gt;&lt;span&gt;ApplicationRecord&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  before_create &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;span&gt;(dog) { dog&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;slug&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; dog&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;kebabcase&lt;/span&gt;&lt;span&gt; } &lt;/span&gt;&lt;span&gt;# .kebabcase doesn&apos;t exist&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  def&lt;/span&gt;&lt;span&gt; to_param&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    slug&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at the docs, I found &lt;code&gt;.underscore&lt;/code&gt; and &lt;code&gt;.dasherize&lt;/code&gt;. Unfortunately, they only work for camel case names:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;DogName&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;underscore&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dasherize&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# =&amp;gt; &quot;dog-name&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But who names their dog in camel case? Maybe people do, but I’m assuming most people use spaces. Unfortunately, &lt;code&gt;.underscore&lt;/code&gt; only works on camel case strings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;Dog Name&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;underscore&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;dasherize&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# =&amp;gt; &quot;dog name&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Searching for a Gem&lt;/h2&gt;
&lt;p&gt;If Ruby and Rails didn’t have a kebab case method. I’m sure there’s a gem out there that does. In comes &lt;a href=&quot;https://github.com/piotrmurach/strings-case/tree/master&quot; target=&quot;_blank&quot;&gt;Strings::Case&lt;/a&gt; to the rescue:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;strings&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;kebabcase&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;PostgreSQL adapter&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# =&amp;gt; &quot;postgre-sql-adapter&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could’ve called it a day here, but looking at the gem, it has so many other methods that I wouldn’t use. Honestly, it seems like overkill for my tiny side project. It’s also another library I would have to maintain and make security updates to. In the end, I decided to roll a custom kebab case method.&lt;/p&gt;
&lt;h2&gt;Rolling a custom kebab case method&lt;/h2&gt;
&lt;p&gt;Before jumping into a solution, I wanted some inspiration from Strings::Case.&lt;/p&gt;
&lt;p&gt;Under the hood, the gem scans through a string, building words determined by whether there is a specific delimiter or if there’s an uppercase. “DogName” would be two words “Dog” and “Name” since there’s an upper case in the middle. And “Dog Name” would be two words, since there’s a space in the middle. These words get put into an array, and after the string has been scanned, a &lt;code&gt;.join&lt;/code&gt; method is called with a separator. In kebab case, the separator would be &lt;code&gt;-&lt;/code&gt;. I’m oversimplifying this approach. If you’re curious, about the source code, you can view it &lt;a href=&quot;https://github.com/piotrmurach/strings-case/blob/master/lib/strings/case.rb#L365&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For the gem, this process makes total sense. It handles many use cases as well as includes multiple string manipulation functions. Since I only wanted kebab case, I decided to go with a regex approach:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;dog&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;downcase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/[^&lt;/span&gt;&lt;span&gt;\w\d\s&lt;/span&gt;&lt;span&gt;]/&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\s&lt;/span&gt;&lt;span&gt;/u&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s break down what this is doing:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;.downcase&lt;/code&gt; - A built-in function that makes the string lowercase.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gsub(/[^\w\d\s]/, &quot;&quot;)&lt;/code&gt; - Regex that matches anything that’s not a character, digit or space, i.e. any special characters and removes it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.gsub(/\s/u, &quot;-&quot;)&lt;/code&gt; - Regex that matches a space and replaces with a dash&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;St. George Bernard the 3rd&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;downcase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/[^&lt;/span&gt;&lt;span&gt;\w\d\s&lt;/span&gt;&lt;span&gt;]/&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\s&lt;/span&gt;&lt;span&gt;/u&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# =&amp;gt; st-george-bernard-the-3rd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The biggest limitation with my approach is the regex. I make plenty of assumptions with what I think names should be, and it’s focused primarily on English. But what if someone uses a character language like Japanese?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&quot;餅&quot;&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;downcase&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/[^&lt;/span&gt;&lt;span&gt;\w\d\s&lt;/span&gt;&lt;span&gt;]/&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;gsub&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;\s&lt;/span&gt;&lt;span&gt;/u&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;-&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# =&amp;gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I wanted to make this more robust, I should handle non-English characters properly. If I can, I could transliterate it. Like &lt;code&gt;é&lt;/code&gt; becomes &lt;code&gt;e&lt;/code&gt;. If it’s in a character language, I could leave it because non-English characters can be used in URLs.&lt;/p&gt;
&lt;p&gt;However, for a small side project, what I have is enough. And I didn’t need to pull in a new gem to achieve it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Using Ruby &lt;code&gt;3.2.0&lt;/code&gt; and Rails &lt;code&gt;7.1.3.3&lt;/code&gt; at time of writing&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>30 Days of Posts</title><link>https://jonathanyeong.com/writing/30-days-of-posts/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/30-days-of-posts/</guid><description>I was inspired to try writing a daily blog post for 30 days</description><pubDate>Fri, 31 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I came across &lt;a href=&quot;https://rachsmith.com/weblogpomo/&quot; target=&quot;_blank&quot;&gt;this blog post&lt;/a&gt; by Rach Smith where they talk about Weblog Posting month. A challenge in May where you post on your blog every day. I love this idea. I haven’t posted on my blog very consistently, mostly due to feeling &lt;a href=&quot;https://jonathanyeong.com/burnout-is-a-sneaky-bugger/&quot; target=&quot;_blank&quot;&gt;burnt out&lt;/a&gt;. So the challenge of posting every day is inspiring! I loved reading through Rach’s posts because they range from technical content to personal thoughts or anecdotes. It makes me feel better about posting whatever.&lt;/p&gt;
&lt;p&gt;Unfortunately, I can’t join the #WeblogPoMo2024 challenge because I’m super late to the party. But I’m going to challenge myself to post every day for the next 30 days. And you might have noticed already that I have a few posts under my belt. I wanted to see if I could get through a week before committing. Under promise, overdeliver.&lt;/p&gt;
&lt;p&gt;You might ask why am I doing this?&lt;/p&gt;
&lt;p&gt;Firstly, I like a challenge. And I need that extra motivation to start posting again.&lt;/p&gt;
&lt;p&gt;Secondly, I want to get better at writing and find my voice. The only way to get better is to force myself to write in public.&lt;/p&gt;
&lt;p&gt;Thirdly, I’m hoping to build a “post brain” (relevant &lt;a href=&quot;https://rachsmith.com/post-brain/&quot; target=&quot;_blank&quot;&gt;Rach Smith post&lt;/a&gt;). I’d like to flex that muscle that tells me when I should write about an experience.&lt;/p&gt;
&lt;p&gt;Finally, I tend to have countless notes in draft which I don’t post because I think they’re stupid, and no one will read them. I’m working really hard to quiet that voice in my head.&lt;/p&gt;
&lt;p&gt;I hope you’ll enjoy the random thoughts that come out of my head!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>You know what they say about assumptions</title><link>https://jonathanyeong.com/writing/you-know-what-they-say-about-assumptions/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/you-know-what-they-say-about-assumptions/</guid><description>I should know better then to assume my code works</description><pubDate>Thu, 30 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Last year, I added a &lt;a href=&quot;https://jonathanyeong.com/changelog-added-comments-to-blog/&quot; target=&quot;_blank&quot;&gt;comments feature to my blog&lt;/a&gt;. During development, I verified the change by checking that the comments section existed on a post. It did, #shipit. After almost a year, someone told me that their comments and reactions weren’t showing. It turns out that I had set up &lt;a href=&quot;https://giscus.app/&quot; target=&quot;_blank&quot;&gt;Giscus&lt;/a&gt; incorrectly from the start 😫! And if that person hadn’t said anything, I would’ve been clueless.&lt;/p&gt;
&lt;p&gt;The fix was trivial, I changed &lt;code&gt;data-categories&lt;/code&gt; in my Giscus setup to point to the correct discussion category &lt;code&gt;&quot;Blog Post Discussions&quot;&lt;/code&gt;, not &lt;code&gt;&quot;Announcements&quot;&lt;/code&gt; (&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/commit/6adde2be2b30b28869367dde2045abe6c942d893&quot; target=&quot;_blank&quot;&gt;here’s the commit&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I took one lesson away from this experience. Something I probably should’ve already known.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just because it’s a personal project doesn’t mean I should take shortcuts with testing&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here’s the test cases I should’ve run through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the comment section load when I view a blog post?&lt;/li&gt;
&lt;li&gt;Can I post a comment?
&lt;ul&gt;
&lt;li&gt;Does the comment section show the new comment?&lt;/li&gt;
&lt;li&gt;Can I put images/gifs in the comment?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Can I add a reaction?
&lt;ul&gt;
&lt;li&gt;Does the reaction show?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;If a comment is posted, do I get a notification?&lt;/li&gt;
&lt;li&gt;Can I reply to a comment?
&lt;ul&gt;
&lt;li&gt;Does the reply show up with the comment?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Writing out the test cases, it feels obvious that I should’ve gone through them. I shouldn’t release something that’s untested. &lt;strong&gt;But these things happen&lt;/strong&gt;. Especially for projects where I’m rushing, tired, or overconfident. Maybe a combination of all three. At least now the comments work, I manually verified the test cases. Happy days.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>What I missed about Ruby</title><link>https://jonathanyeong.com/writing/what-i-missed-about-ruby/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-i-missed-about-ruby/</guid><description>I had a 2 year break from Ruby, and I have so many things that I missed and need to catch up on</description><pubDate>Wed, 29 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’m back in Ruby on Rails land after two years using only Node/TS. And I’m thrilled to be back! I feel like I missed so much progress with Rails 7 over the last 2 years. But now that I’m back, I can finally use all the things I missed. Here’s all the things that I took for granted before my break:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Convention is enforced in Rails. Organization is much easier in a rails project, and I’m able to hop into any Rails project and find my way around.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Some&lt;/em&gt; magic is nice. Such as auto-loading files!&lt;/li&gt;
&lt;li&gt;Rails console!!! Omg being able to run code snippets is amazing. I downloaded &lt;a href=&quot;https://runjs.app/&quot; target=&quot;_blank&quot;&gt;RunJS&lt;/a&gt;to be able to achieve the same thing. But it’s not a replacement for being able to play around with the data along with your code.&lt;/li&gt;
&lt;li&gt;Mocking with RSpec is way easier than Jest. In Node/TS, we used dependency injection to be able to mock classes and methods. But it was annoying to use dependency injection when it was only used in testing. &lt;em&gt;Please let me know if there’s a better way to do it&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Having Rails routes to get an overview of all the entry points to the app is so underrated.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One thing that I took away from the last two years, is typing. I’m not going to lie. I like it. It’s nice to be able to hover over an object and know what fields exist in it. I would love with Ruby to be able to hover over a hash and see all the keys that exist. I added &lt;a href=&quot;https://sorbet.org/&quot; target=&quot;_blank&quot;&gt;Sorbet&lt;/a&gt; and &lt;a href=&quot;https://github.com/ruby/rbs&quot; target=&quot;_blank&quot;&gt;RBS&lt;/a&gt; to my things to learn list.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Taking charge of my onboarding: my personal checklist</title><link>https://jonathanyeong.com/writing/personal-onboarding-checklist/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/personal-onboarding-checklist/</guid><description>My onboarding checklist that I&apos;ve been using over the past few jobs to help give me structure when I&apos;ve recently joined a company</description><pubDate>Tue, 28 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When I first join a company, I need some sort of structure. I feel like there’s an ocean of information, and I’m told to start rowing. It’s overwhelming to know where to start.&lt;/p&gt;
&lt;p&gt;Thankfully, most companies have some sort of onboarding when you first join. It gives you some sort of foundation, maybe even helps you get the application setup on your machine. But often times I find company onboarding lacking guidance beyond the first couple of weeks.&lt;/p&gt;
&lt;p&gt;To help me feel like I’m progressing, I’ve made my own personal onboarding checklist that I’ve used and tweaked over the past few companies I’ve worked at. I try to tick these items off during my first month of onboarding.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt; Join all the fun and team related Slack channels&lt;/li&gt;
&lt;li&gt; Set up 1-1s with my entire team.
&lt;ul&gt;
&lt;li&gt; Get to know them; topics include where are they based, how long have they been on the team, any surprises or challenges they’ve faced so far&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Set up a recurring 1-1 with my onboarding buddy (hopefully done earlier, and hopefully, I’m assigned one)&lt;/li&gt;
&lt;li&gt; Set up a recurring 1-1 with my manager (hopefully they do it)&lt;/li&gt;
&lt;li&gt; Read through the company career ladder, understand what’s required from me. Summarise these points and share it with my manager in our first 1-1.
&lt;ul&gt;
&lt;li&gt; Understand what my manager &lt;em&gt;thinks&lt;/em&gt; success looks like&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Set up the application locally, run it, and click around the interface.&lt;/li&gt;
&lt;li&gt; Have someone take me through the application. At a high level, happy path flow, understand how a user uses the application.
&lt;ul&gt;
&lt;li&gt; Perform user actions myself, after the meeting. I wish to see what are the critical classes, any logging, and what the data looks like in the database.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Get access to staging and production data. I want to see what real-world data looks like.
&lt;ul&gt;
&lt;li&gt; Get access to any other tools required for my role (hopefully done during general company onboarding)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; Understand the coding workflow, either have someone take me through it or find some docs. What are the company conventions?&lt;/li&gt;
&lt;li&gt; Pair on a feature / bug&lt;/li&gt;
&lt;li&gt; Complete a feature / bug solo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hopefully, some of these tasks are included during onboarding. Is there anything I should add?&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Tools I Use: Excalidraw</title><link>https://jonathanyeong.com/writing/tools-i-use-excalidraw/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/tools-i-use-excalidraw/</guid><description>Sharing my favourite whiteboarding tool</description><pubDate>Mon, 27 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;My brain is terrible at visualizing and tracking how various concepts fit together. &lt;a href=&quot;https://excalidraw.com/&quot; target=&quot;_blank&quot;&gt;Excalidraw&lt;/a&gt; helps make my brain be better. It’s a &lt;strong&gt;free&lt;/strong&gt; low-fidelity whiteboarding tool that I’ve been using for system design interviews, brainstorming, flow diagrams, wireframe/mockups. Anything that I used to draw on a piece of paper is now being done in Excalidraw. It has basically everything you need to diagram, and if you need more, there is a plugin library to get extra shapes.&lt;/p&gt;
&lt;p&gt;The low fidelity look and feel reduces the friction of creating diagrams. I don’t need to worry about perfection, I can slap together a few shapes add some text, and I’ve mapped out what was in my head.&lt;/p&gt;
&lt;p&gt;Here are a couple of random things I’ve made in Excalidraw:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jonathanyeong/personal-website/assets/3861088/55838772-f679-413d-ab1e-453bbc5c3f14&quot; alt=&quot;Flow chart of how my personal website is structured&quot; /&gt;
&lt;em&gt;Flow chart of how my personal website is structured&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/jonathanyeong/home-alone/blob/main/home_alone_design_v2.png?raw=true&quot; alt=&quot;Brainstorming a side project&quot; /&gt;
&lt;em&gt;Brainstorming a side project I’m working on&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>What is Zeitwerk?</title><link>https://jonathanyeong.com/writing/what-is-zeitwerk/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-is-zeitwerk/</guid><description>A note on Zeitwerk, a code loader for Ruby.</description><pubDate>Sun, 26 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/fxn/zeitwerk&quot; target=&quot;_blank&quot;&gt;Zeitwerk&lt;/a&gt; is a gem that loads Ruby files. From Rails 6, Zeitwerk is the default loader. Loading a Ruby file means making that file available in your code. There are a few ways to do this with Ruby, &lt;code&gt;require&lt;/code&gt;, &lt;code&gt;require_relative&lt;/code&gt;, &lt;code&gt;load&lt;/code&gt;. Zeitwerk removes the need to add these special keywords and does the loading for you. It can load files on demand (autoloading) or upfront (eager loading).&lt;/p&gt;
&lt;p&gt;Zeitwerk works by following naming conventions. The idea is that file paths match the constant name.  From Zeitwerk’s README:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;lib/my_gem.rb         -&amp;gt; MyGem&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;lib/my_gem/foo.rb     -&amp;gt; MyGem::Foo&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;lib/my_gem/bar_baz.rb -&amp;gt; MyGem::BarBaz&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;lib/my_gem/woo/zoo.rb -&amp;gt; MyGem::Woo::Zoo&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One reason you would use Zeitwerk in your Ruby projects is to get rid of using &lt;code&gt;require&lt;/code&gt;. Manually loading files is brittle because you might forget to load certain files or load files in an incorrect order. Zeitwerk also removes the need for &lt;code&gt;require_dependency&lt;/code&gt; since Zeitwerk also supports code reloading.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>What I look for in a company</title><link>https://jonathanyeong.com/writing/what-i-look-for-in-a-company/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-i-look-for-in-a-company/</guid><description>A list of red and green flags that I look for when interviewing at a company</description><pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I went through months of interviews recently. And one question that I got asked repeatedly was “what do you look for in a company?” or “what are you looking for in your next role?”. For some of these interviews, I gave some pretty generic answers:&lt;/p&gt;
&lt;p&gt;“I’d like to work on challenging problems”
“I’m looking for a company with an inclusive culture”
“I want a team where I can learn and grow”&lt;/p&gt;
&lt;p&gt;A few times I had to stop myself saying:&lt;/p&gt;
&lt;p&gt;“I dunno, money I guess?”&lt;/p&gt;
&lt;p&gt;Of course, I want to work in an inclusive company, where I’m growing and working on fun, challenging problems. And of course I want to get paid but reflecting on this question. I’m realizing I had more answers to that question.&lt;/p&gt;
&lt;p&gt;I have green flags, what will make me say yes to more interviews or yes to an offer. And red flags, what will make me say hell no!&lt;/p&gt;
&lt;h2&gt;Red Flags&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Messy, unorganized recruiting process&lt;/li&gt;
&lt;li&gt;Recruiters that take weeks to respond to your emails. Or send you roles that don’t match at all with your skill set.&lt;/li&gt;
&lt;li&gt;Interviewers show up uninterested, late, derail you from completing the interview, or are taking Slack messages during your interview.&lt;/li&gt;
&lt;li&gt;When recruiters / interviewers don’t know the answer to questions about the company like with profitability, market fit, road map, cash runway, etc. Approximate answers are okay, but if they won’t tell you outright or don’t know the answer to these questions, then red flag.&lt;/li&gt;
&lt;li&gt;Microservices IF the dev tooling is subpar or non-existent. Also, cookie cutter answers to why use microservices.&lt;/li&gt;
&lt;li&gt;Long deployment cycle. I want to deploy at any time of the day every day.&lt;/li&gt;
&lt;li&gt;If they don’t support fully remote work, ESPECIALLY if their team is distributed&lt;/li&gt;
&lt;li&gt;If the team doesn’t have a spread of developer levels, is mainly top (senior+) heavy, and doesn’t have a plan for hiring devs across levels.&lt;/li&gt;
&lt;li&gt;Crypto companies&lt;/li&gt;
&lt;li&gt;Non-AI companies talking about integrating some AI being the next big thing they’re working on.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Green Flags&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;When salary ranges are shared ahead of time before recruiter calls. I.e. in the job posting.&lt;/li&gt;
&lt;li&gt;Coding questions aren’t hacker rank / leet code style questions. And they feel more like pair programming.&lt;/li&gt;
&lt;li&gt;When developers talk about the product with excitement. And are proud of what they’ve released.&lt;/li&gt;
&lt;li&gt;When recruiters send you tips and what to expect before an interview. Or hop on a call to prep you for that interview.&lt;/li&gt;
&lt;li&gt;Humour. If I can joke with my interviewer, and we share a laugh, green flag.&lt;/li&gt;
&lt;li&gt;If the company has contributed to open source or open sources their own projects. If they give developers time to contribute to open source as well.&lt;/li&gt;
&lt;li&gt;If benefits like learning and development exist. Is it formalized and part of company culture (green flag)? Or is it an afterthought (red flag)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These flags are pretty personal to me. What do you agree or disagree with? What are your red / green flags?&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Burnout is a sneaky bugger</title><link>https://jonathanyeong.com/writing/burnout-is-a-sneaky-bugger/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/burnout-is-a-sneaky-bugger/</guid><description>Burnout sucks, and I found it hard to spot when I was going through it</description><pubDate>Fri, 24 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Burnout really sneaked up on me. Would not recommend 0/10.&lt;/p&gt;
&lt;p&gt;It took me a week into my 5-week break between jobs to realise that I was actually burnt out. And it took almost the entire break to feel energised again.&lt;/p&gt;
&lt;p&gt;I had always read about burnout, and I had seen my peers go through it. I get it now. It totally freaking sucks. All I could do after work was stumble over to the couch and watch easy-to-digest TV. &lt;em&gt;Side note: I’m pretty good at picking out the killer in crime shows now&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In hindsight, the signs were pretty obvious. But when you’re in it, it’s hard to tell what’s burnout or what’s fatigue. There are some great resources on burnout, this one on &lt;a href=&quot;https://codesubmit.io/blog/developer-burnout/&quot; target=&quot;_blank&quot;&gt;CodeSubmit is my fav&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m starting to feel more energised these days, and with that brings a whole lot of excitement about learning new things and engaging with my communities. I’m lucky that I had a chance to take a long break to recover. If you have that time, I recommend it. Especially, if you’re able to get out of a toxic work environment in the process. My goal is to take more time out of the day to be kind to oneself. I hope you’ll join me!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Fixing a Timezone bug on my blog</title><link>https://jonathanyeong.com/writing/fixing-blog-timezone-bug/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/fixing-blog-timezone-bug/</guid><description>I had a bug on my blog where the published date displayed was different then the published date I added in my markdown. It came down to a timezone issue!</description><pubDate>Mon, 17 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I noticed a weird bug on my blog the other day. The dates that I set in my blog post metadata didn’t line up with the dates that I saw on the page. It was always a day off. For example, the front matter for my blog post showed days:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 2023-07-11&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 2023-07-10&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the dates displayed on my blog post was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;Published Jul 10, 2023 | Updated Jul 9, 2023&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This bug felt like a timezone error. It’s weird that it was off by one day. But I wasn’t sure, so I went through my debug steps.&lt;/p&gt;
&lt;p&gt;I console logged &lt;code&gt;pubDate&lt;/code&gt; and &lt;code&gt;updatedDate&lt;/code&gt; everywhere and I got these values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;updatedDate is: 2023-07-10T00:00:00.000Z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pubDate is: 2023-07-11T00:00:00.000Z&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Which meant I wasn’t doing some weird conversion when pulling out the metadata from my blog post.&lt;/p&gt;
&lt;p&gt;The only place where it was changing the date was in this snippet:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  date&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toLocaleDateString&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;en-us&apos;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    year&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;numeric&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    month&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;short&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    day&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;numeric&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  })&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So I dug into the &lt;code&gt;toLocalDateString&lt;/code&gt; docs. It turns out &lt;code&gt;toLocaleDateString&lt;/code&gt; was using my local timezone. I had totally missed this line from the &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString&quot; target=&quot;_blank&quot;&gt;MDN docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;strong&gt;&lt;code&gt;toLocaleDateString()&lt;/code&gt;&lt;/strong&gt; method returns a string with a language-sensitive representation of the date portion of the &lt;strong&gt;specified date in the user agent’s timezone&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unfortunately, there didn’t seem like an easy way to keep the UTC timezone when displaying the dates with &lt;code&gt;toLocaleDateString()&lt;/code&gt;. Since my date formatting needs were pretty basic, I could use &lt;code&gt;toUTCString()&lt;/code&gt; instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (date&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; dateParts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; date&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;toUTCString&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;split&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot; &quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; monthIndex &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 2&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; dayIndex &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; yearIndex &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; 3&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; `&lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;dateParts[monthIndex]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; ${&lt;/span&gt;&lt;span&gt;dateParts[dayIndex]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;${&lt;/span&gt;&lt;span&gt;dateParts[yearIndex]&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s not pretty but it works without needing to pull in another library. If I wanted to have fancier date formatting I could use something like &lt;a href=&quot;https://date-fns.org/&quot; target=&quot;_blank&quot;&gt;date-fns&lt;/a&gt;. But I found a gotcha with date-fns&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since &lt;code&gt;date-fns&lt;/code&gt; always returns a plain JS Date, which implicitly has the current system’s time zone, helper functions are needed for handling common time zone related use cases. (&lt;a href=&quot;https://date-fns.org/v2.30.0/docs/Time-Zones&quot; target=&quot;_blank&quot;&gt;source&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So if I wanted to display a UTC time on my current system which is set to EST. Then I would need to pull in &lt;a href=&quot;https://github.com/marnusw/date-fns-tz&quot; target=&quot;_blank&quot;&gt;date-fns-tz&lt;/a&gt; instead. This library lets me specify which IANA timezone and uses &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl&quot; target=&quot;_blank&quot;&gt;the Intl API&lt;/a&gt;. Here’s what that change would look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; { formatInTimeZone } &lt;/span&gt;&lt;span&gt;from&lt;/span&gt;&lt;span&gt; &apos;date-fns-tz&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; formatDate&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (date&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Date&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; formatInTimeZone&lt;/span&gt;&lt;span&gt;(date, &lt;/span&gt;&lt;span&gt;&apos;Etc/UTC&apos;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&apos;MMM dd, yyyy&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This bug was a reminder that timezones are the worst.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;P.S: I had this post sitting in draft for about a year, and I have no idea why.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Adding Topics (aka tags) in Astro</title><link>https://jonathanyeong.com/writing/adding-topics-in-astro/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/adding-topics-in-astro/</guid><description>A short how to guide on adding topics to your Astro site. Part of Virtual Coffee Build in Public challenge</description><pubDate>Tue, 11 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This month’s Virtual Coffee challenge is &lt;a href=&quot;https://dev.to/virtualcoffee/join-virtual-coffee-for-the-build-in-public-the-power-of-daily-standup-and-demo-challenge-35kb&quot; target=&quot;_blank&quot;&gt;Build in Public&lt;/a&gt;. And I wanted to kick things off with some website updates. I’ve been meaning to add tags to my site for a long time. After poking around at other people’s sites, I decided to switch from the concept of tagging to the concept of topics. While the naming is different the process to adding topics was the same. In this post I want to share how I added topics, what I learnt, and what’s next.&lt;/p&gt;
&lt;h2&gt;How I added topics to my site&lt;/h2&gt;
&lt;p&gt;I mainly followed the &lt;a href=&quot;https://docs.astro.build/en/tutorial/5-astro-api/3/&quot; target=&quot;_blank&quot;&gt;Astro guide on building a tag index page&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Firstly, I created this structure in my project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;pages/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  topics/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    [topic].astro&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    index.astro&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;index.astro&lt;/code&gt; is rendered when you go to &lt;code&gt;/topics&lt;/code&gt; (&lt;a href=&quot;https://jonathanyeong.com/topics/&quot; target=&quot;_blank&quot;&gt;example link&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[topic].astro&lt;/code&gt; is rendered when you go to &lt;code&gt;/topics/:topic&lt;/code&gt;. Where &lt;code&gt;:topic&lt;/code&gt; is something I define in my blog posts. Example, &lt;code&gt;topics/api%20design/&lt;/code&gt; (&lt;a href=&quot;https://jonathanyeong.com/topics/api%20design/&quot; target=&quot;_blank&quot;&gt;example link&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I then added the &lt;code&gt;topics&lt;/code&gt; field to my front matter yml in my blog posts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;pubDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 2023-04-05&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;updatedDate&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; 2023-04-05&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;title&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;template&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;description&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;A template&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;draft&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Added &lt;code&gt;topics&lt;/code&gt; key in my &lt;code&gt;config.ts&lt;/code&gt; file (&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/content/config.ts#L21&quot; target=&quot;_blank&quot;&gt;source code&lt;/a&gt;)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; z&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .&lt;/span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;(z&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;())&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .&lt;/span&gt;&lt;span&gt;optional&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  .&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;([])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, to display the topics on my blog post, I made a component that is pulled into the Blog Post Layout:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/layouts/BlogPost.astro#L25&quot; target=&quot;_blank&quot;&gt;BlogPost.astro&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{topics &lt;/span&gt;&lt;span&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; topics&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;length &lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt; 0&lt;/span&gt;&lt;span&gt; &amp;amp;&amp;amp;&lt;/span&gt;&lt;span&gt; &amp;lt;&lt;/span&gt;&lt;span&gt;TopicList&lt;/span&gt;&lt;span&gt; topics&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; /&amp;gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/components/TopicList.astro&quot; target=&quot;_blank&quot;&gt;TopicList.astro&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const { topics } = Astro.props;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;---&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;flex space-x-3&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;{topics.map((topic: string) =&amp;gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;`&lt;/span&gt;&lt;span&gt;/topics/${topic}`}&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;span&gt;{topic}&lt;/span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;))}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Deep dive (aka what did I learn)&lt;/h3&gt;
&lt;p&gt;Digging into the &lt;code&gt;index.astro&lt;/code&gt; and &lt;code&gt;[topic].astro&lt;/code&gt; files a bit more.&lt;/p&gt;
&lt;h4&gt;index.astro&lt;/h4&gt;
&lt;p&gt;The way we get a list of topics to display on the &lt;code&gt;/topic&lt;/code&gt; page is through this code (&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/pages/topics/index.astro#L9&quot; target=&quot;_blank&quot;&gt;source code&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; posts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;blog&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; topics &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; Set&lt;/span&gt;&lt;span&gt;(posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;flatMap&lt;/span&gt;&lt;span&gt;((post) &lt;/span&gt;&lt;span&gt;=&amp;gt;&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;topics))];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This line will get all my posts from my blog folder, pull out all of the topics, and return an array of topics with no duplicates.&lt;/p&gt;
&lt;h4&gt;[topic].astro&lt;/h4&gt;
&lt;p&gt;To generate the corresponding topic endpoints like &lt;code&gt;/topic/api%20design/&lt;/code&gt; I needed to modify the &lt;code&gt;getStaticPaths()&lt;/code&gt; function in the &lt;code&gt;[topic].astro&lt;/code&gt;. (&lt;a href=&quot;https://github.com/jonathanyeong/personal-website/blob/main/src/pages/topics/%5Btopic%5D.astro#L7&quot; target=&quot;_blank&quot;&gt;source code&lt;/a&gt;). I forgot to modify it originally, and I was very confused when I kept getting a 404 not found error when trying to navigate to a topic page 🤦🏻!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; getStaticPaths&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; posts&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; getCollection&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;blog&apos;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; topics&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; Set&lt;/span&gt;&lt;span&gt;(posts&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;flatMap&lt;/span&gt;&lt;span&gt;((post)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;data&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;topics))];&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; topics&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;((topic)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; ({&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    params&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;span&gt; topic&lt;/span&gt;&lt;span&gt; }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.astro.build/en/reference/api-reference/#getstaticpaths&quot; target=&quot;_blank&quot;&gt;getStaticPaths()&lt;/a&gt; is an Astro function that Astro uses to generate each page at build time. It returns an array that looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  { params&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {topic&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;API Design&quot;&lt;/span&gt;&lt;span&gt;} },&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  { params&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; {topic&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &quot;Productivity&quot;&lt;/span&gt;&lt;span&gt;} }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What’s next&lt;/h2&gt;
&lt;p&gt;I’m not super happy with how my topics are styled. But I’m also not sure how I would style them - I’m open to suggestions! Next up for me is writing an About me page. I don’t know why but I find it incredibly hard to write! I’ll be chugging away at it while trying to come up with a better styling in the meantime.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>What is the right amount of testing?</title><link>https://jonathanyeong.com/writing/what-is-the-right-amount-of-testing/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-is-the-right-amount-of-testing/</guid><description>My opinions on good defaults to keep in mind when trying to understand the right amount of testing</description><pubDate>Sun, 11 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;What is the right amount of testing?&lt;/p&gt;
&lt;p&gt;Over the years my answer to this question has changed drastically. From fully embracing integration testing and requiring code coverage numbers on Pull Requests (PRs). To writing only unit tests covering the critical paths of the application. Leaving much of code untested.&lt;/p&gt;
&lt;p&gt;Of course there’s no definitive answer for the right amount of testing. If there was, everyone would be doing it. Instead, I’d like to share what I consider a good default amount of testing. But before diving into that, let’s discuss the problem we’re trying to solve.&lt;/p&gt;
&lt;p&gt;The primary goal of testing is to prevent bugs from reaching production. Too little testing leads to a brittle application. As more features get pushed out, more fires need to be fought. And eventually you’re worried that any change might cause something unrelated to break.&lt;/p&gt;
&lt;p&gt;In response, we swing the pendulum to the other side. We write extensive suites of integration tests that get run on every PR. Code coverage numbers need to be met before a PR can be approved. We aim to test every possible path, leaving no room for bugs to slip through. Unfortunately, our feedback loops begins to slow down. And meaningful features take longer to be released.&lt;/p&gt;
&lt;p&gt;Both of these are extreme ends of the spectrum. Too little testing leads to brittle applications. Exhaustive integration tests slow down feedback loops and delay feature releases. So what is a happy middle ground? Here’s what I think we should default to.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Write unit tests for all your changes. Testing error paths but not every combination of errors/inputs. Pick your edge cases that cover critical aspects of your code. Here’s some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Test paths that have a complex outcome and/or an error you display to the user.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; condition&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  complex_outcome&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;else&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  error_to_user&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;In general, you don’t need to test well-established, trusted libraries. But you should manually validate library calls (see more on manual testing below).&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Don&apos;t test that Rails will save a model to the database.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;model &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Model&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Don&apos;t test S3 library will delete the bucket&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;s3Client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;delete_bucket&lt;/span&gt;&lt;span&gt;({  &lt;/span&gt;&lt;span&gt;bucket&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;bucketName&quot;&lt;/span&gt;&lt;span&gt;, })&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Test for public not private methods. Unexpected changes to private methods should be caught in the public method tests.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# Test this method&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; public_method&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  something &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; do_something&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  do_something_with&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;something&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;private&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Don&apos;t test these methods&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; do_something&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;def&lt;/span&gt;&lt;span&gt; do_something_with&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Test suites should run fast enough that by the time you make a coffee your build is complete (faster is better). That means unit tests over integration tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If possible, always manually test your change. Keep the process lightweight and repeatable by writing down your steps. Don’t test the entire app behaviour on every change. But allow someone on your team to vet your change by running through the testing steps you wrote down. If you find yourself testing the same thing over and over again, look to automate it (i.e. write a unit test).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Manually testing is especially important if testing a third party service/api. Mocking in unit tests is needed for fast tests but you need to validate your mocks in the real world. Validating third party services/api is the purpose of integration tests. But arguably, a quick manual test may be all that’s needed along with good observability.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bonus points if you have a cloud developer environment or a developer preview on your PR that allows someone to validate your changes without having to run your code on their machine. This approach also promotes collaboration between your team!&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, have a robust CI/CD pipeline that lets you rollback bad builds, or feature flags to quickly turn off a breaking change. Having the right monitors in place will tell you if something is broken before it causes too much damage. Expect that bugs will happen even with a proper amount of testing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While there’s no one size fits all solution for the right amount of testing. I’ve found that these defaults have worked well in the past. They strike a balance between maintaining healthy developer feedback loops while minimizing game breaking bugs that make it to production.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Developer productivity with Github Codespaces</title><link>https://jonathanyeong.com/writing/developer-productivity-with-github-codespaces/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/developer-productivity-with-github-codespaces/</guid><description>My journey into improving my development workflow</description><pubDate>Fri, 19 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;During my time at Shopify, I had the opportunity to work with a cloud development environment - and I was sold. Since then, I set out trying to recreate a similar experience for my personal development. Enter, &lt;a href=&quot;https://github.com/features/codespaces&quot; target=&quot;_blank&quot;&gt;Github Codespaces&lt;/a&gt;. I’d like to share my thoughts on using Codespaces and why I believe it will greatly benefit developer productivity.&lt;/p&gt;
&lt;h2&gt;Exploring Codespaces: My Motivations&lt;/h2&gt;
&lt;p&gt;There are a couple of reasons why I decided to explore GitHub Codespaces:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Escape from Bare Metal Setup&lt;/strong&gt;: I’m tired of installing and managing specific versions of Ruby, Node, Postgresql across my various projects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Onboarding Developers&lt;/strong&gt;: one of my aspirations is to run my own company, I wanted to explore how cloud development environments like Codespaces could facilitate the smooth onboarding of new engineers.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Github Codespaces Workflow&lt;/h2&gt;
&lt;p&gt;Firstly, install the Github CLI tool. Being able to use the terminal over the browser has significantly streamlined my experience. Once authenticated with the Github CLI, my workflow is as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Creating a Codespace:&lt;/strong&gt; Initially, I needed to create a Codespace. Note that if you cloned a project and you’re in that folder, the Github CLI tool will use that project as the default&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;gh&lt;/span&gt;&lt;span&gt; codespace&lt;/span&gt;&lt;span&gt; create&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Getting to Work:&lt;/strong&gt; To start working on my application, I ran the following command, selecting the Codespace I had just created. There’s a short delay as the environment is being set up:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;gh&lt;/span&gt;&lt;span&gt; codespace&lt;/span&gt;&lt;span&gt; code&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Astro Aside:&lt;/strong&gt; Since I’m running the Codespace for my Astro site, I made an update to the &lt;code&gt;dev&lt;/code&gt; command in the &lt;code&gt;package.json&lt;/code&gt; file:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;astro&lt;/span&gt;&lt;span&gt; dev&lt;/span&gt;&lt;span&gt; --host&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This configuration binds the Astro local server to any IP, allowing proper connection when Codespace forwards the localhost port.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Codespace Logging:&lt;/strong&gt; I also experimented with &lt;a href=&quot;https://github.com/jonathanyeong/dotfiles&quot; target=&quot;_blank&quot;&gt;Dotfiles&lt;/a&gt; for my Codespace, which customize the Codespace developer environment. The Codespace logs proved invaluable for debugging my changes as the Codespace started:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;gh&lt;/span&gt;&lt;span&gt; codespace&lt;/span&gt;&lt;span&gt; logs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Initial Considerations&lt;/h2&gt;
&lt;p&gt;Based on my early experience, here are a few considerations regarding Codespaces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Overkill for Simple Projects&lt;/strong&gt;: Codespaces is overkill for simple projects like my Astro blog. Given the minimal version dependencies and longer startup time compared to setting up my Astro project directly, it doesn’t give that much benefit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat Codespaces as Ephemeral&lt;/strong&gt;: It’s important to treat Codespaces as ephemeral environments. That means commit often! Any uncommitted changes will be lost when the Codespace is destroyed (which occurs by default every 30 days).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Untapped Potential&lt;/h2&gt;
&lt;p&gt;I’m totally bought into the idea of cloud development environments. Github Codespace is one option there’s also others such as &lt;a href=&quot;https://www.gitpod.io/&quot; target=&quot;_blank&quot;&gt;Gitpod&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Imagine this situation, you’re starting a new job, excitedly setting up a fresh codebase, only to encounter a mysterious, undocumented error. You turn to your onboarding buddy for help. They speak the dreaded words, “weird - works on my machine”. A response that leaves you navigating a labyrinth of debugging all by your lonesome. Okay I’m being a little dramatic, but we’ve all experienced these “works on my machine” moments that can send you spiraling.&lt;/p&gt;
&lt;p&gt;With a cloud developer environment like Codespaces, someone can get their project up and running in minutes rather then days. Think about the time and headache saved! These environments can be used for more then onboarding too. You could use them as developer previews, sharing a URL of your app running in the Codespace so anyone can test your changes.&lt;/p&gt;
&lt;p&gt;While I haven’t found a project to fully utilize Codespaces yet, I’m psyched to keep exploring it. Stay tuned, I’ll keep this post updated.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Learning about API First Design</title><link>https://jonathanyeong.com/writing/what-is-api-first-design/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-is-api-first-design/</guid><description>Learning note on what API First design is and why you&apos;d use it</description><pubDate>Mon, 08 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Building an API using an API first approach means that the API is the core deliverable. The API is treated like a core product that other products build on top of. Rather then products being built first then an API is slapped on top of them. There’s another term - API design first which is focused on &lt;em&gt;how&lt;/em&gt; you design your API. Whereas with API-first you focus on both design and building the API first.&lt;/p&gt;
&lt;p&gt;I think of it like a restaurant. With API First you define exactly how the wait staff will send orders to the kitchen. In this case, we could say that wait staff will &lt;em&gt;only&lt;/em&gt; use numbers and then the kitchen will have some mapping on their end to figure out what dish to cook. With API design first, the wait staff and kitchen will come together and figure out what the language is around an order. They may come up with a set of rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;These set of abbreviations will mean this (e.g. bf = beef)&lt;/li&gt;
&lt;li&gt;Numbers are okay or not. The kitchen will have some mapping on their end.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In API design first, it’s all about communication. While API first is more rigid in how the API is defined in the first place. In both examples, building and API was about defining a contract - there are overlaps between the two approaches.&lt;/p&gt;
&lt;p&gt;It feels like API first and API design first really go hand in hand. Building an API that interfaces with other teams means you need to discuss design even if you’re building API first. I would love some examples where this isn’t the case!&lt;/p&gt;
&lt;p&gt;Finally, one way to describe your API/create a contract is through a specification language like &lt;a href=&quot;/what-is-open-api&quot;&gt;OpenAPI&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Resources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.stoplight.io/api-first-vs.-api-design-first-a-comprehensive-guide&quot; target=&quot;_blank&quot;&gt;API-First vs. API Design-First: A Comprehensive Guide by Spotlight&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/articles/design-first-api-development/&quot; target=&quot;_blank&quot;&gt;Design-First Approach to API Development: How to Implement and Why It Works by InfoQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Redesigning my Blog: 2023</title><link>https://jonathanyeong.com/writing/blog-redesign-2023/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/blog-redesign-2023/</guid><description>Just gave my blog a fresh new look for 2023! Switching from SvelteKit to Astro - read on to find out why!</description><pubDate>Wed, 06 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you’ve been here before you might have noticed a few things have changed. That’s right, I blew up my blog and rebuilt it from ground up! Two years ago I wrote this line in a &lt;a href=&quot;/blog-redesign-2021&quot;&gt;previous blog redesign post&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I heard of this new Static site generator, Astro that looks very interesting…
Nope. Not going down that path.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well my friend, I moved from &lt;a href=&quot;https://kit.svelte.dev/&quot; target=&quot;_blank&quot;&gt;SvelteKit&lt;/a&gt; to &lt;a href=&quot;https://astro.build/&quot; target=&quot;_blank&quot;&gt;Astro&lt;/a&gt; 😅. On top of a framework change, I also changed my design to embrace minimalism. I wanted to focus on content and nothing else, being heaviy inspired by &lt;a href=&quot;https://lethain.com/&quot; target=&quot;_blank&quot;&gt;Irrational Exuberance&lt;/a&gt;. In my previous site, I fell into the trap of spending my energy building features that weren’t the core reason of why someone would come to my site. With the stage set, let’s talk through some of my thoughts so far on the rewrite:&lt;/p&gt;
&lt;h3&gt;Moving fast with Astro &amp;amp; Tailwind&lt;/h3&gt;
&lt;p&gt;Getting up and running with Astro was a really nice experience. For the first time, I feel like I have a static site generator that is straightforward to use. But offers the additional flexibility to add more reactive features using &lt;a href=&quot;https://docs.astro.build/en/concepts/islands/&quot; target=&quot;_blank&quot;&gt;Astro islands&lt;/a&gt;. No shade to SvelteKit though! I loved it but I felt like it was too much for my needs. I’m trying to embrace minimalism in all aspects, including framework features.&lt;/p&gt;
&lt;p&gt;Another thing that Astro gives is typing on my Markdown frontmatter. I love this feature! It means that I can have build time errors if I have a typo or if I’m missing a frontmatter key. You can read more about &lt;a href=&quot;https://docs.astro.build/en/guides/content-collections/#defining-a-collection-schema&quot; target=&quot;_blank&quot;&gt;defining collection schemas on the Astro docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also moved to &lt;a href=&quot;https://tailwindcss.com/&quot; target=&quot;_blank&quot;&gt;Tailwind&lt;/a&gt; for this rewrite and deleted most of my CSS in the process. Tailwind is a utility first CSS framework that allows you to style an element through class names. For example, here’s how my links are styled:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt; href&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt; class&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;no-underline font-semibold dark:text-teal-400 text-logo-green before:bg-teal-600 before:dark:bg-teal-300 before:rounded-lg hover:before:opacity-100 hover:dark:text-teal-300 hover:text-teal-600 relative&quot;&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  Link&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/&lt;/span&gt;&lt;span&gt;a&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Honestly, the upside of not having to maintain my css far outweighs the downside of poor readibility on elements with lots of classes. Getting Astro setup with Tailwind was a breeze with the &lt;code&gt;@astrojs/tailwind&lt;/code&gt; &lt;a href=&quot;https://docs.astro.build/en/guides/integrations-guide/tailwind/&quot; target=&quot;_blank&quot;&gt;integration&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Changing URL namespacing&lt;/h3&gt;
&lt;p&gt;I moved away from the &lt;code&gt;/garden/&amp;lt;blog-title&amp;gt;&lt;/code&gt; hierarchy and flattened it to &lt;code&gt;/&amp;lt;blog-title&amp;gt;&lt;/code&gt;. Swyx writes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Personal sites of frequent speakers often opt for TWO namespaces: /writing and /speaking. My current site (as of Sept 2020) uses this, but I have grown to dislike it: I will often speak about what I write, and write about related things I speak about. Why separate them? It is more effort to make my talks discoverable to my readers and vice versa.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In his article &lt;a href=&quot;https://www.swyx.io/namespacing-sites&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;Against Namespacing Personal Sites&lt;/em&gt;&lt;/a&gt;. And while I agree with this idea, I tried to overcome that by having a &lt;code&gt;garden/&lt;/code&gt; namespace. But I’ll admit, the naming is horrible. If you don’t know what a &lt;a href=&quot;https://maggieappleton.com/garden-history&quot; target=&quot;_blank&quot;&gt;digital garden&lt;/a&gt; is then a &lt;code&gt;garden/&lt;/code&gt; namespace makes no sense. You can set this up in Astro by adding a &lt;code&gt;[...slug].astro&lt;/code&gt; file at the root of &lt;code&gt;src/pages&lt;/code&gt; folder.&lt;/p&gt;
&lt;h3&gt;Generating OpenGraph (OG) Images on the fly&lt;/h3&gt;
&lt;p&gt;Honestly, this is my favourite part of the redesign. I’m using &lt;a href=&quot;https://docs.netlify.com/edge-functions/overview/&quot; target=&quot;_blank&quot;&gt;Netlify Edge functions&lt;/a&gt; to generate OG images on the fly. This image will show up when you share this post on Twitter (or Facebook but who still uses that). Shoutout to Kevin Zuniga Cuellar, whoe wrote about &lt;a href=&quot;https://www.kevinzunigacuellar.com/blog/og-images-on-the-edge/&quot; target=&quot;_blank&quot;&gt;og images on the edge&lt;/a&gt;. The annoying parts of building this were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Debugging was painful because there wasn’t great error messages when something broke. And every change took a second of build time so the feedback loop was pretty longer then I wanted.&lt;/li&gt;
&lt;li&gt;You can’t access folders with Netlify Edge functions which meant that I can’t use custom fonts unless they’re loaded on some CDN.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;I’m trying to get better at building in public so this site is far from finished. I want to focus on content next. Over time, I’ll be adding pages for tags, featured and an about page. If you’re curious, here’s a screenshot of the old homepage and you can still play with the old site &lt;a href=&quot;https://elastic-engelbart-26e6d9.netlify.app/&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img /&gt;&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Learning about Open API</title><link>https://jonathanyeong.com/writing/what-is-open-api/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/what-is-open-api/</guid><description>Learning note on what Open API is and what it&apos;s used for</description><pubDate>Tue, 05 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;OpenAPI is a specification (OpenAPI Specification (OAS)) that is a way to define how you interact with an API without needing to understand implementation logic. You describe the capabilities of the service and someone can understand what it does without needing to look at source code, documentation, or network traffic inspection. The OpenAPI definition can then be used by documentation tools or code gen tools to display the docs or generate servers and clients respectively.&lt;/p&gt;
&lt;p&gt;In other words, OAS is a standardized way of describing your API that isn’t tied to any programming language. A reason you would write an OAS document is to help during the API design lifecycle (&lt;a href=&quot;https://www.openapis.org/what-is-openapi&quot; target=&quot;_blank&quot;&gt;reference&lt;/a&gt;). You can use this document with a tool such as &lt;a href=&quot;https://www.useoptic.com/docs/diff-openapi&quot; target=&quot;_blank&quot;&gt;Optic&lt;/a&gt; to prevent things like breaking changes from your implementation versus the design.&lt;/p&gt;
&lt;h3&gt;Backlinks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/what-is-api-first-design&quot;&gt;Learning about API First Design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Redesigning my Blog: 2021</title><link>https://jonathanyeong.com/writing/blog-redesign-2021/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/blog-redesign-2021/</guid><description>A walkthrough of how this site is built! From the inception to switching it on in production</description><pubDate>Tue, 09 Nov 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;What will you learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What technology can be used to build a blog&lt;/li&gt;
&lt;li&gt;The process I have when building a site&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;TL;DR - This site is built with SvelteKit and hosted on Netlify. It’s open sourced, &lt;a href=&quot;https://github.com/jonathanyeong/home-on-the-web&quot; target=&quot;_blank&quot;&gt;take a gander&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Some time ago, I embarked on a journey to redesign my website. I convinced myself that I needed a fresh, clean, minimalistic, snazzy, whimsical, &lt;code&gt;&amp;lt;insert adjective&amp;gt;&lt;/code&gt; site to be able to write content.&lt;/p&gt;
&lt;p&gt;While this reasoning is flawed. Rebuilding my site allowed me to play with SvelteKit. A framework for Svelte that was fresh out of beta when I first started the rebuild. It was also an opportunity to practice my design skills. Opting this time around to design most of my screens in Figma &lt;em&gt;before&lt;/em&gt; building.&lt;/p&gt;
&lt;p&gt;Throughout this entire process I made heaps of mistakes and discovered what worked and what didn’t. All the lessons that I’m excited to share!&lt;/p&gt;
&lt;h2&gt;Designing upfront&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tools&lt;/strong&gt;: &lt;a href=&quot;https://www.figma.com/&quot; target=&quot;_blank&quot;&gt;Figma&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resources:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.refactoringui.com/?ref=biblidev.fr&quot; target=&quot;_blank&quot;&gt;Refactoring UI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/channel/UCQsVmhSa4X-G3lHlUtejzLA&quot; target=&quot;_blank&quot;&gt;Figma Youtube Channel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leveluptutorials.com/tutorials/design-systems-in-figma/getting-started-and-auto-layout&quot; target=&quot;_blank&quot;&gt;Level Up Tuts: Design Systems in Figma&lt;/a&gt; (Paid)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://designcode.io/ui-design&quot; target=&quot;_blank&quot;&gt;Design + Code: UI Design for Developers&lt;/a&gt; (Paid)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m no design guru so I can’t speak too much to colour theory, typography and other design-y stuff. But here’s what worked for me.&lt;/p&gt;
&lt;h3&gt;Start with the layout&lt;/h3&gt;
&lt;p&gt;I focused initially on building the layout of my site (both mobile and desktop) using grayscale and blocks. I went around to all the sites that I found aesthetically pleasing and took inspiration of their layouts. Then I used these blocks to play around with how my content would be structured.&lt;/p&gt;
&lt;p&gt;This was focused only on the homepage since I thought that would be the most visually complex page. But I would have found value doing it for the posts page as well.&lt;/p&gt;
&lt;p&gt;What I was looking for was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Visual hierarchy - do the blocks flow and fit together.&lt;/li&gt;
&lt;li&gt;Spacing - Is there room for the blocks to breathe or does it feel cramped?&lt;/li&gt;
&lt;li&gt;Focus - Is there too much information presented at once?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1637208804/personal-blog/Screen_Shot_2021-11-16_at_9.52.20_PM_l2spki.png&quot; alt=&quot;Layouts done in grayscale with blocks&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Setting up a design system&lt;/h3&gt;
&lt;p&gt;The next thing I set up was a design system. This system standardized the colours and the fonts that I would be using across my design. Building this upfront saved a bunch of time later on. Changes to font sizing or colour could be done in one place and it would adjust throughout all my design files. During this phase, I used &lt;a href=&quot;https://type-scale.com/&quot; target=&quot;_blank&quot;&gt;Type Scale&lt;/a&gt; to define my font sizes and line heights. And then a Figma plugin, &lt;a href=&quot;https://www.figma.com/community/plugin/733159460536249875/A11y---Color-Contrast-Checker&quot; target=&quot;_blank&quot;&gt;A11y - Color Contrast Checker&lt;/a&gt;, helped me find accessible font colours for light and dark backgrounds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1637208804/personal-blog/Screen_Shot_2021-11-16_at_9.57.16_PM_isi4jd.png&quot; alt=&quot;Building a design system&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Going from grey blocks to pretty colours&lt;/h3&gt;
&lt;p&gt;After picking a layout, it was only a matter of replacing those gray blocks with actual content. I used components in Figma to standardize what a Post card would look like. You can also give properties to components. This means you can define components in different active states or theme options, like my post cards which have both a dark and a light theme property.&lt;/p&gt;
&lt;p&gt;The goal at this stage was to have my site look as close as possible to the finished product. This step took time. A shit ton of time. Much of it was spent figuring out how to use Figma. But there was also time spent second guessing my design and tinkering with different layouts. Ultimately, these making these decisions up front meant that I could spend more time working on the build.&lt;/p&gt;
&lt;h2&gt;The Tech Stack&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/&quot; target=&quot;_blank&quot;&gt;Svelte Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kit.svelte.dev/&quot; target=&quot;_blank&quot;&gt;SvelteKit Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://svelte.dev/chat&quot; target=&quot;_blank&quot;&gt;Svelte Discord&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://leveluptutorials.com/tutorials/svelte-kit/what-is-svelte-kit&quot; target=&quot;_blank&quot;&gt;Level Up Tuts: SvelteKit&lt;/a&gt; (Paid)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I have a tendency to get distracted by shiny things. And this blog is no exception. When I found out that SvelteKit was coming into public beta, I jumped on the bandwagon. This involved a &lt;strong&gt;full&lt;/strong&gt; rewrite of my site. I was coming from 11ty and nothing survived the transition.&lt;/p&gt;
&lt;p&gt;If you’re familiar with React and Next.js - SvelteKit offers similar sensibilities. It offers both server side and client side rendering. A built in router. Developer environment optimizations. And Svelte, which has been a joy to use.&lt;/p&gt;
&lt;p&gt;Styling is done using SCSS, a css preprocessor. Out of the box, styles in Svelte are scoped per component. Posts are processed using &lt;a href=&quot;https://mdsvex.com/&quot; target=&quot;_blank&quot;&gt;Mdsvex&lt;/a&gt;. This package is similar to &lt;a href=&quot;https://mdxjs.com/&quot; target=&quot;_blank&quot;&gt;MDX&lt;/a&gt; for React. It lets me define components and import them into my markdown. For example, this alert box is a Svelte component! I just moved &lt;a href=&quot;/blog-redesign-2023.md&quot;&gt;my site to Astro&lt;/a&gt; so this won’t work anymore.&lt;/p&gt;
&lt;p&gt;I can then import the Alert box into any of my markdown files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  import AlertBox from &quot;$lib/AlertBox.svelte&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;# Markdown header&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;Some paragraph text&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&amp;lt;AlertBox content=&quot;Hiii I&apos;m a Svelte component!&quot; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the search, I use Fuse.js to do the fuzzy search matching. The search and tag filtering probably warrants it’s own post. &lt;em&gt;If you’re interested &lt;a href=&quot;https://twitter.com/JonoYeong&quot; target=&quot;_blank&quot;&gt;bug me&lt;/a&gt;&lt;/em&gt;. But I’ll give a quick rundown of it. Fuse.js takes in all my posts, and keys off my post titles and tags. In other words, you can search for both titles and tags. Svelte will then react to typing, by using a &lt;em&gt;reactive declaration&lt;/em&gt; &lt;code&gt;$:&lt;/code&gt;,  and it will call Fuse to return results based on the current search. This search should also be scoped to the currently selected tags (or all tags if none are selected). To be honest, I’m not entirely happy with how I built this. It works but it feels… wrong. I’m currently in the process of &lt;a href=&quot;https://github.com/jonathanyeong/home-on-the-web/pull/37&quot; target=&quot;_blank&quot;&gt;refactoring my search&lt;/a&gt;, but if you want to see it in it’s current state - the &lt;a href=&quot;https://github.com/jonathanyeong/home-on-the-web/blob/main/src/routes/garden/index.svelte#L57&quot; target=&quot;_blank&quot;&gt;code lives here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, the site is deployed on Netlify which has been the hosting platform I’ve used since my 11ty days. It’s been great (and free) for my use case. SvelteKit has a &lt;a href=&quot;https://github.com/sveltejs/kit/tree/master/packages/adapter-netlify&quot; target=&quot;_blank&quot;&gt;Netlify Adapter&lt;/a&gt; that creates a Netlify function for dynamic server side rendering. Along with hooking into Netlify fun-ness like Netlify redirect rules and forms.&lt;/p&gt;
&lt;h2&gt;Retrospective&lt;/h2&gt;
&lt;h3&gt;Decisions made&lt;/h3&gt;
&lt;p&gt;Other than the search and tagging functionality, this build was pretty straightforward. But even so, there are decisions that I made that are hard to reverse. I wanted to share these in case you’re facing a similar decision!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I was thinking about using TailwindCSS and decided against it. Instead, I rolled my own plain SCSS. The main reason behind this was speed. Both in terms of reducing package load during development or production. And also the time it would take to learn Tailwind. Scoped styles in Svelte makes maintaining CSS less cognitively demanding. You don’t need to think about the cascades in terms of the application. Only the component.&lt;/li&gt;
&lt;li&gt;Choosing SvelteKit was done out of passion (or FOMO). But one reason I wanted to move off 11ty in the first place was to have greater interactivity. I was inspired by Josh Comeau’s site which is full of whimsy. That is something that I wanted to strive for with SvelteKit. It’s a work in progress for my site.&lt;/li&gt;
&lt;li&gt;I spent a little time researching other hosting platforms (&lt;a href=&quot;https://vercel.com/&quot; target=&quot;_blank&quot;&gt;Vercel&lt;/a&gt;, &lt;a href=&quot;https://render.com/&quot; target=&quot;_blank&quot;&gt;Render&lt;/a&gt;, &lt;a href=&quot;https://begin.com/&quot; target=&quot;_blank&quot;&gt;Begin&lt;/a&gt;), but went with Netlify in the end. It was what I was used to. And I was happy with it’s free tier. Another reason why I decided to stick with Netlify was DNS records. DNS is a pain to manage, and I don’t really want to deal with it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Challenges Faced&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Documentation, documentation, documentation. SvelteKit was newly public when I started using it. And I spent a lot of time being stuck. I was grateful for the Svelte community for answering my questions. But working on this project made me appreciate the importance of docs.&lt;/li&gt;
&lt;li&gt;It took my a very long time to ingest the Markdown files. Specifically, it was how to load them and where I should be loading them. I can chalk this up to not understanding how SvelteKit &amp;amp; Svelte works.&lt;/li&gt;
&lt;li&gt;Battling Flash of Unstyled text (FOUT). FOUT is the default text that appears for a moment while your site loads in the css files. I still haven’t fixed this. Initially, I attempted to not load the site until the css files were loaded. However, this fix ended up causing an issue where I never server side rendered my website. In other words, if you didn’t have javascript loaded, all you would see is a blank page!&lt;/li&gt;
&lt;li&gt;The search &amp;amp; tag functionality. I was pretty happy with myself when I got this working. I think it’s overly complex and in need of a refactor. But I spent a lot of time playing around with stores and tinkering with Fuse.js.&lt;/li&gt;
&lt;li&gt;Metatags were surprisngly hard to add! I think I was spoilt coming from 11ty with Nunjucks. What I really wanted was a default set of metatags that I could override within individual pages. In the end, I duplicated the metatag information across multiple files. Thinking about it now, there has to be a better way to do this.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Surprises&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Writing Svelte is a joy. Most of it is plain HTML and CSS, and the components are structured similar to Vue. Having scoped CSS out of the box is also incredible. Some of my favourite Svelte features are built in stores and reactive declarations. They’re a little weird at first but they cut down on so much boilerplate code!&lt;/li&gt;
&lt;li&gt;How much SvelteKit gets out of your way when developing. From the project setup prompts, to adding new packages and features, to the build times, it all feels… svelte.&lt;/li&gt;
&lt;li&gt;The weirdness of having two script tags in a single component. Also, how quickly I got used to it. If you have two script tags, one (denoted by &lt;code&gt;context=&apos;module&apos;&lt;/code&gt;) is executed during server side rendering, while the other is executed during client side. If you have one script tag, that code is executed both in server side rendering and client side.&lt;/li&gt;
&lt;li&gt;Time it took for the build. Estimating is hard and it will always be a perpetual surprise. Over time, I’ve gotten better at it. Like sometimes I can tell when someone is about to surprise me because they have this look in their eyes. But I still can’t predict what they’re going to do.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What’s next?&lt;/h2&gt;
&lt;p&gt;I heard of this new Static site generator, &lt;a href=&quot;https://astro.build/blog/introducing-astro/&quot; target=&quot;_blank&quot;&gt;Astro&lt;/a&gt; that looks very interesting…&lt;/p&gt;
&lt;p&gt;Nope. Not going down that path.&lt;/p&gt;
&lt;p&gt;What’s next is to populate this site with more content. I’m also working on a better flow to get my posts from draft to publish. Doing some refactoring, lots of polishing. And more whimsy, because you can never have enough. If you’re curious you can view my &lt;a href=&quot;https://github.com/users/jonathanyeong/projects/1/views/4&quot; target=&quot;_blank&quot;&gt;project roadmap&lt;/a&gt; and you can see the source code for &lt;a href=&quot;https://github.com/jonathanyeong/home-on-the-web&quot; target=&quot;_blank&quot;&gt;my site here&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Excerpts with Eleventy</title><link>https://jonathanyeong.com/writing/excerpts-with-eleventy/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/excerpts-with-eleventy/</guid><description>A tutorial on getting excerpts into 11ty.</description><pubDate>Fri, 28 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I opened a &lt;a href=&quot;https://github.com/11ty/eleventy/issues/1380&quot; target=&quot;_blank&quot;&gt;Github issue&lt;/a&gt; for this all the way back in Aug 2020. Here’s a better approach then mine (&lt;a href=&quot;https://github.com/11ty/eleventy/issues/1380#issuecomment-698457560&quot; target=&quot;_blank&quot;&gt;linked comment&lt;/a&gt;):&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;.eleventy.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;eleventyConfig.setFrontMatterParsingOptions({ excerpt: true });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;{{ article.data.page.excerpt | md | safe }}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;md&lt;/code&gt; filter looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;// .eleventy.js&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; markdownIt &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;markdown-it&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;// ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;eleventyConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addFilter&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;md&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; (content&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; markdownIt&lt;/span&gt;&lt;span&gt;({&lt;/span&gt;&lt;span&gt; html&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; true&lt;/span&gt;&lt;span&gt; })&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;render&lt;/span&gt;&lt;span&gt;(content);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;I 100% struggled with getting excerpts in &lt;a href=&quot;https://www.11ty.dev/&quot; target=&quot;_blank&quot;&gt;Eleventy&lt;/a&gt; (11ty). It seems so simple, but I couldn’t figure it out.&lt;/p&gt;
&lt;p&gt;I had two issues. Firstly, I got excerpts working using &lt;a href=&quot;https://www.11ty.dev/docs/data-frontmatter-customize/#example-parse-excerpts-from-content&quot; target=&quot;_blank&quot;&gt;11ty’s custom frontmatter data&lt;/a&gt;, unfortunately the excerpt did not render markdown. Which resulted in something that looked like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1598592524/personal-blog/Screen_Shot_2020-08-27_at_9.32.53_PM_eq15uh.png&quot; alt=&quot;Post excerpt that is not rendering links properly&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Secondly, I found a great blog post: &lt;a href=&quot;https://keepinguptodate.com/pages/2019/06/creating-blog-with-eleventy/&quot; target=&quot;_blank&quot;&gt;Creating a blog with Eleventy&lt;/a&gt; that detailed how to create an excerpt shortcode. This shortcode defaulted to taking the first paragraph as an excerpt. Or whatever custom excerpt tag you wanted. However, the shortcode ended up rendering the HTML tags. The opposite problem to 11ty’s excerpt. Rendering HTML resulted in some weird looking excerpts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1598592542/personal-blog/Screen_Shot_2020-08-27_at_12.50.24_AM_nzvnwt.png&quot; alt=&quot;Post excerpt that is rendering links but it&apos;s adding extra html elements&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I could not find a happy medium! Here’s what I really wanted out of an excerpt:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cap at 200 characters&lt;/li&gt;
&lt;li&gt;Strip any HTML tags.&lt;/li&gt;
&lt;li&gt;Add an ellipses at the end of the excerpt&lt;/li&gt;
&lt;li&gt;Automatically pull the excerpt, no need for specific tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thankfully, the blog post above gave me all the code I needed. I just had to make a few modifications.&lt;/p&gt;
&lt;h2&gt;The modifications to extractExcerpt&lt;/h2&gt;
&lt;p&gt;First, install &lt;a href=&quot;https://www.npmjs.com/package/striptags&quot; target=&quot;_blank&quot;&gt;striptags&lt;/a&gt;. We will be using this library to get rid of the HTML tags:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;npm&lt;/span&gt;&lt;span&gt; install&lt;/span&gt;&lt;span&gt; striptags&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we want to require &lt;code&gt;striptags&lt;/code&gt; in &lt;code&gt;.eleventy.js&lt;/code&gt;. At the top of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; striptags &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; require&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;striptags&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, add the &lt;code&gt;extractExcerpt&lt;/code&gt; method at the bottom of the &lt;code&gt;.eleventy.js&lt;/code&gt; file. This method is a modified version from the blog post to fit the criteria I wanted:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; extractExcerpt&lt;/span&gt;&lt;span&gt;(article)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;article&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;hasOwnProperty&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;templateContent&quot;&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    console&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;warn&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &apos;Failed to extract excerpt: Document has no property &quot;templateContent&quot;.&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    );&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  let&lt;/span&gt;&lt;span&gt; excerpt&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; null&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; content&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; article&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;templateContent;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  excerpt&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; striptags&lt;/span&gt;&lt;span&gt;(content)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;substring&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;0&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; 200&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; // Cap at 200 characters&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;replace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;^&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;+|&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;$&lt;/span&gt;&lt;span&gt;|&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt;(?=&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;s&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt;g&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;trim&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    .&lt;/span&gt;&lt;span&gt;concat&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;...&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; excerpt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add this line in the &lt;code&gt;eleventyConfig&lt;/code&gt; block. This step is also from the blog post:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;module&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;exports&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt;(eleventyConfig)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  eleventyConfig&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addShortcode&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;excerpt&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; (article)&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; extractExcerpt&lt;/span&gt;&lt;span&gt;(article));&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, we can see our results.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1598592567/personal-blog/Screen_Shot_2020-08-27_at_1.00.25_AM_rmwf9a.png&quot; alt=&quot;Fixed example of what the post excerpt should be rendering as&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Hooray! Nothing looks too off. I do see &lt;code&gt;Addendum #&lt;/code&gt; which is caused by &lt;code&gt;markdownIt&lt;/code&gt; adding an anchor permalink to the header. I can live with it though.&lt;/p&gt;
&lt;p&gt;Looking back on this problem, the solution seems so easy. But I spent almost a week trying to figure out how to do this properly. I think it’s a combination of being stubborn with wanting the 11ty custom frontmatter solution to work, and my inability to Google. Really need to Google or ask for help sooner. I don’t know about you, but knowing when to ask for help is an endless struggle for me. Bar this one issue, I’m loving 11ty so far! Hopefully, this post will solve your excerpt woes.&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>How to setup CI with Phoenix and Github Actions</title><link>https://jonathanyeong.com/writing/how-to-setup-ci-with-phoenix-github-actions/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/how-to-setup-ci-with-phoenix-github-actions/</guid><description>Tutorial on getting CI set up with Phoenix and Github Actions. Tests are automatically run anytime you push a commit to Github.</description><pubDate>Fri, 24 Jul 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;⚠️ This post may contain outdated content&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Continuous Integration (CI) is the act of automating the build and testing of your code when it’s committed to version control. For a small app like my blog, I wanted to setup CI without needing to create accounts with &lt;a href=&quot;https://travis-ci.org/&quot; target=&quot;_blank&quot;&gt;TravisCI&lt;/a&gt; or &lt;a href=&quot;https://circleci.com/&quot; target=&quot;_blank&quot;&gt;CircleCI&lt;/a&gt;. This is where &lt;a href=&quot;https://github.com/features/actions&quot; target=&quot;_blank&quot;&gt;Github Actions&lt;/a&gt; comes to the rescue. Actions is a workflow automation tool and has a variety of different uses. You could send an SMS, build and push a docker image, run code coverage, and much more. Today, I want to show you how to use Github Actions to setup a CI pipeline for your Phoenix app.&lt;/p&gt;
&lt;h2&gt;Create the Workflow&lt;/h2&gt;
&lt;p&gt;This tutorial assumes, you already have a Phoenix app and you’re using Github as your repository. In your project, create a file under &lt;code&gt;&amp;lt;your-phoenix-app&amp;gt;/.github/workflows&lt;/code&gt;. Choose any name for your file and save it with the the yaml (.&lt;code&gt;yml&lt;/code&gt;) extension e.g. &lt;code&gt;phoenix_test.yml&lt;/code&gt;. Add the code below to the file. Before we commit and push let’s understand what’s actually happening.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;# &amp;lt;your-phoenix-app&amp;gt;/.github/workflows/phoenix_text.yml&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Elixir CI&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  push&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  workflow_dispatch&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  run-tests&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    container&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; node:10.18-jessie&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      postgres&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        image&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres:12&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;5432:5432&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          POSTGRES_PASSWORD&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        options&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          --health-cmd pg_isready&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          --health-interval 10s&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          --health-timeout 5s&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          --health-retries 5&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; actions/checkout@v2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; actions/setup-elixir@v1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          elixir-version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;1.10.3&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          otp-version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;22.3&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix deps.get&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix deps.compile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; cd assets &amp;amp;&amp;amp; yarn install --frozen-lockfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix test&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          DB_PASSWORD&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          DB_HOST&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Code Breakdown&lt;/h2&gt;
&lt;p&gt;Before we dive in, let’s quickly touch on the basic structure of an Action. A Github Action consists of a &lt;strong&gt;workflow&lt;/strong&gt;. A workflow has many &lt;strong&gt;jobs&lt;/strong&gt;. And a job is made up of &lt;strong&gt;steps&lt;/strong&gt;. In the code above, our workflow is the entire yml file. We’ve named our workflow &lt;code&gt;Elixir CI&lt;/code&gt;. Inside our workflow we have one job, &lt;code&gt;run-tests&lt;/code&gt;. If we had multiple jobs, they would run in parallel. Inside a job we have many steps. Each step will run sequentially. Once all the steps have finished our job will be complete. Once all the jobs have finished our workflow will be complete. And once our workflow is done Github will report the results. Now that we know the structure, let’s move on.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Elixir CI&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  push&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pull_request&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    branches&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [ &lt;/span&gt;&lt;span&gt;master&lt;/span&gt;&lt;span&gt; ]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  workflow_dispatch&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#name&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;name&lt;/code&gt;&lt;/a&gt; defines the name of our workflow. You will see this name when you view your action on Github.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestpaths&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;on&lt;/code&gt;&lt;/a&gt; defines the events that trigger your workflow. In this case, we will run our workflow when we push to master. Or when a pull request is opened against master.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/events-that-trigger-workflows#workflow_dispatch&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;workflow_dispatch&lt;/code&gt;&lt;/a&gt; allows us to manually trigger our builds. This key is optional. Since we have a pretty simple workflow, we don’t really need this. But it can be helpful for debugging. Note, that there’s no value. If the key exists, Github will configure everything properly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;jobs&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  run-tests&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    runs-on&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ubuntu-latest&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    container&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; node:10.18-jessie&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobs&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/a&gt; defines the jobs that your workflow runs. We only have one job and it’s called &lt;code&gt;run-tests&lt;/code&gt;. You could name your jobs anything.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;runs-on&lt;/code&gt;&lt;/a&gt; declares what virtual machine your Github Action runs on. Your options are Mac, Windows, or Linux. Most of the time you would run on Linux. But for some cases you might want to run Windows or Mac. For example, if you needed to convert a Microsoft powerpoint to a PDF, you would have to run your Action on windows. An important concept to remember is that every Github Action runs on a virtual machine.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontainer&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;container&lt;/code&gt;&lt;/a&gt; declares containers for our VM. A Github Action will spin up a virtual machine (VM). Whatever you run will be run on that machine. In this case, we are adding node to our VM via a container.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;services&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  postgres&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    image&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres:12&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ports&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;&apos;5432:5432&apos;&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      POSTGRES_PASSWORD&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    options&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &amp;gt;&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      --health-cmd pg_isready&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      --health-interval 10s&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      --health-timeout 5s&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      --health-retries 5&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idservices&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;services&lt;/code&gt;&lt;/a&gt; declares the tools needed to run our task. These tools are installed via a container, with networking already configured. In our case we want to setup Postgres for our tests to run on. We do this by pulling in the &lt;code&gt;postgres:12&lt;/code&gt; container. We expose the port &lt;code&gt;5432&lt;/code&gt; and set our password to &lt;code&gt;postgres&lt;/code&gt;. Finally, we do a health-check on the database to make sure it’s started correctly by passing in some &lt;code&gt;options&lt;/code&gt;. &lt;a href=&quot;https://www.postgresql.org/docs/current/app-pg-isready.html&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;pg_isready&lt;/code&gt;&lt;/a&gt; is a utility command found in Postgres that checks the connection status of the server. &lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;&amp;gt;-&lt;/code&gt; is a special yml syntax. It means anything after &lt;code&gt;&amp;gt;-&lt;/code&gt; is combined as one line with no newline character (&lt;code&gt;\\n&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;steps&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; actions/checkout@v2&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; uses&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; actions/setup-elixir@v1&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    with&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      elixir-version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;1.10.3&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      otp-version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &apos;22.3&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix deps.get&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix deps.compile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; cd assets &amp;amp;&amp;amp; yarn install --frozen-lockfile&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  -&lt;/span&gt;&lt;span&gt; run&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; mix test&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    env&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      DB_PASSWORD&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      DB_HOST&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; postgres&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally to the fun part! Everything before &lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idsteps&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;steps&lt;/code&gt;&lt;/a&gt; was needed to set up our environment. Now we can actually do things. You’ll notice 4 keywords here: &lt;code&gt;uses&lt;/code&gt;, &lt;code&gt;with&lt;/code&gt;, &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;env&lt;/code&gt;. Let’s go through each one.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsuses&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;uses&lt;/code&gt;&lt;/a&gt; selects an action to run as a step. Github has a &lt;a href=&quot;https://github.com/marketplace?type=actions&quot; target=&quot;_blank&quot;&gt;rich ecosystem of actions&lt;/a&gt;. And we can reuse these actions in our own workflow. An example of this is the &lt;code&gt;actions/checkout@v2&lt;/code&gt;. The &lt;a href=&quot;https://github.com/actions/checkout&quot; target=&quot;_blank&quot;&gt;checkout action&lt;/a&gt; pulls our code down from Github onto the VM.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;with&lt;/code&gt;&lt;/a&gt; passes input parameters to the external Action. In the case of &lt;code&gt;actions/setup-elixir@v1&lt;/code&gt; we are passing in the elixir version and the Erlang OTP version. &lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;setup-elixir&lt;/code&gt; works best if you specify the exact versions.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;run&lt;/code&gt;&lt;/a&gt; executes a command line command.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsenv&quot; target=&quot;_blank&quot;&gt;&lt;code&gt;env&lt;/code&gt;&lt;/a&gt; sets environment variables on that command. For example, when running &lt;code&gt;mix test&lt;/code&gt; we will set the &lt;code&gt;DB_PASSWORD&lt;/code&gt; and the &lt;code&gt;DB_HOST&lt;/code&gt; environment variables.&lt;/p&gt;
&lt;p&gt;Knowing these keywords, we can read our steps like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pull down our codebase using a checkout action.&lt;/li&gt;
&lt;li&gt;Install Elixir v1.10.3 and Erlang OTP v22.3 using the setup-elixir action.&lt;/li&gt;
&lt;li&gt;Get the dependencies for our Phoenix app.&lt;/li&gt;
&lt;li&gt;Compile the dependencies.&lt;/li&gt;
&lt;li&gt;Install the frontend assets using yarn.&lt;/li&gt;
&lt;li&gt;Run our tests with two environment variables &lt;code&gt;DB_PASSWORD&lt;/code&gt; and &lt;code&gt;DB_HOST&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gotcha&lt;/h3&gt;
&lt;p&gt;Since we’re running everything inside the docker container, the Postgres service will need a &lt;code&gt;postgres&lt;/code&gt; hostname. If you’re like me, you may have configured &lt;code&gt;test.exs&lt;/code&gt; to connect to your local database with different credentials. To get the &lt;code&gt;mix test&lt;/code&gt; step working you can edit your &lt;code&gt;test.exs&lt;/code&gt; to include the &lt;code&gt;System.get_env&lt;/code&gt; command. Where &lt;code&gt;app&lt;/code&gt; is your Phoenix application name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;config :app, &lt;/span&gt;&lt;span&gt;App&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Repo&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  username: &lt;/span&gt;&lt;span&gt;&quot;postgres&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  password: &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;DB_PASSWORD&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &quot;localhost&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  database: &lt;/span&gt;&lt;span&gt;&quot;app_test&lt;/span&gt;&lt;span&gt;#{&lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;MIX_TEST_PARTITION&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  hostname: &lt;/span&gt;&lt;span&gt;System&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;get_env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;DB_HOST&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;||&lt;/span&gt;&lt;span&gt; &quot;localhost&quot;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  pool: &lt;/span&gt;&lt;span&gt;Ecto&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Adapters&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Sandbox&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Running the Workflow&lt;/h2&gt;
&lt;p&gt;Commit your changes and push to Github. Now go to Github and navigate to the Actions tab. Here, you will see any current or past workflow runs. Every run will have a commit, the branch the commit was on, and some other metadata.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1596067038/personal-blog/screen-shot-2020-07-19-at-1-58-18-pm_qnr5ah.png&quot; alt=&quot;Github Actions page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;If you wanted to manually run your workflow you will have to click into the workflow. My workflow name is &lt;code&gt;Elixir CI&lt;/code&gt; so clicking into it gives me the following page. On this page, you can run your workflow by clicking the button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://res.cloudinary.com/jonathan-yeong/image/upload/v1596067038/personal-blog/screen-shot-2020-07-19-at-1-58-38-pm_jqm758.png&quot; alt=&quot;See the Run Workflow button to manually run your action&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;What’s next&lt;/h2&gt;
&lt;p&gt;Now every time you commit your code your tests will run and you will be notified if anything broke! The next evolution would be to automatically deploy our application to production (continuous deployment).&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item><item><title>Building a blog with Phoenix: Getting started</title><link>https://jonathanyeong.com/writing/building-a-blog-with-phoenix-part-one/</link><guid isPermaLink="true">https://jonathanyeong.com/writing/building-a-blog-with-phoenix-part-one/</guid><description>The first installment of a tutorial to get a blog up and running with Phoenix. Inspired by the Ruby on Rails tutorails on getting a blog running</description><pubDate>Thu, 09 Jul 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today, I want to show you how to start building a blog using Phoenix. You can find the source code here: &lt;a href=&quot;https://github.com/jonathanyeong/phoenix_blog/tree/v0.0.1&quot; target=&quot;_blank&quot;&gt;https://github.com/jonathanyeong/phoenix_blog/tree/v0.0.1&lt;/a&gt;. We are using Phoenix version 1.5.3 and Elixir version 1.10.3. We will cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#project-setup&quot;&gt;Project Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#generate-migration&quot;&gt;Generate migration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#add-routes&quot;&gt;Add Routes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#add-the-post-controller&quot;&gt;Add the Post Controller&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#breakdown&quot;&gt;Breakdown&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#add-post-view--templates&quot;&gt;Add Post View &amp;amp; Templates&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#breakdown-1&quot;&gt;Breakdown&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;p&gt;After installing Phoenix and Elixir (see &lt;a href=&quot;https://hexdocs.pm/phoenix/installation.html#content&quot; target=&quot;_blank&quot;&gt;Phoenix docs&lt;/a&gt;). Setup your new blog application.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  mix phx.new phoenix_blog&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  cd phoenix_blog/&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  mix ecto.create&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Generate migration&lt;/h2&gt;
&lt;p&gt;Firstly, we need to create the Post table. We can use the mix task &lt;code&gt;phx.gen.schema&lt;/code&gt; to generate a schema and a migration file (&lt;a href=&quot;https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Schema.html&quot; target=&quot;_blank&quot;&gt;see the docs&lt;/a&gt; for more info). The schema is how we represent information from the database within our application. While, the migration contains the commands that run on the database itself.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    mix phx.gen.schema Post posts content:text title:string&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    mix ecto.migrate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Add Routes&lt;/h2&gt;
&lt;p&gt;Now that we’ve run the migration, we need to add routes to our app so that we can view and create posts on the front-end. We will be using &lt;code&gt;resources&lt;/code&gt; to generate resourceful routes for us.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;lib/phoenix_blog_web/router.ex&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  scope &quot;/&quot;, PhoenixBlogWeb do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    resources &quot;/posts&quot;, PostController&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you run &lt;code&gt;mix phx.routes&lt;/code&gt;. You should see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  post_path  GET     /                                      PhoenixBlogWeb.PostController :index&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  GET     /posts                                 PhoenixBlogWeb.PostController :index&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  GET     /posts/:id/edit                        PhoenixBlogWeb.PostController :edit&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  GET     /posts/new                             PhoenixBlogWeb.PostController :new&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  GET     /posts/:id                             PhoenixBlogWeb.PostController :show&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  POST    /posts                                 PhoenixBlogWeb.PostController :create&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  PATCH   /posts/:id                             PhoenixBlogWeb.PostController :update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;              PUT     /posts/:id                             PhoenixBlogWeb.PostController :update&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  post_path  DELETE  /posts/:id                             PhoenixBlogWeb.PostController :delete&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if you start the Phoenix server&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  mix phx.server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And navigate to &lt;code&gt;localhost:4000/posts&lt;/code&gt; you will get an error. Let’s fix this.&lt;/p&gt;
&lt;h2&gt;Add the Post Controller&lt;/h2&gt;
&lt;p&gt;All of these routes point to a nonexistent Post Controller. Lets create one and add the following code.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  # lib/phoenix_blog_web/controllers/post_controller.ex&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  defmodule&lt;/span&gt;&lt;span&gt; PhoenixBlogWeb&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;PostController&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    use&lt;/span&gt;&lt;span&gt; PhoenixBlogWeb&lt;/span&gt;&lt;span&gt;, :controller&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    alias&lt;/span&gt;&lt;span&gt; PhoenixBlog&lt;/span&gt;&lt;span&gt;.{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      Post&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      Repo&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    def&lt;/span&gt;&lt;span&gt; index&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conn&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; _params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    posts &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Post&lt;/span&gt;&lt;span&gt; |&amp;gt;&lt;/span&gt;&lt;span&gt; Post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;ordered&lt;/span&gt;&lt;span&gt;() &lt;/span&gt;&lt;span&gt;|&amp;gt;&lt;/span&gt;&lt;span&gt; Repo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      render&lt;/span&gt;&lt;span&gt;(conn, &lt;/span&gt;&lt;span&gt;&quot;index.html&quot;&lt;/span&gt;&lt;span&gt;, posts: posts)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    def&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conn&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; _params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      changeset &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;changeset&lt;/span&gt;&lt;span&gt;(%&lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;{}, %{})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      render&lt;/span&gt;&lt;span&gt;(conn, &lt;/span&gt;&lt;span&gt;&quot;new.html&quot;&lt;/span&gt;&lt;span&gt;, changeset: changeset)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    def&lt;/span&gt;&lt;span&gt; create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;conn&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; %&lt;/span&gt;&lt;span&gt;{&lt;/span&gt;&lt;span&gt;&quot;post&quot;&lt;/span&gt;&lt;span&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; post_params&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; _params&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    changeset &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; Post&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;changeset&lt;/span&gt;&lt;span&gt;(%&lt;/span&gt;&lt;span&gt;Post&lt;/span&gt;&lt;span&gt;{}, post_params)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    case&lt;/span&gt;&lt;span&gt; Repo&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;insert&lt;/span&gt;&lt;span&gt;(changeset) &lt;/span&gt;&lt;span&gt;do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      {:ok, &lt;/span&gt;&lt;span&gt;_log&lt;/span&gt;&lt;span&gt;} &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        |&amp;gt;&lt;/span&gt;&lt;span&gt; put_flash&lt;/span&gt;&lt;span&gt;(:info, &lt;/span&gt;&lt;span&gt;&quot;Success - created a Post!&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        |&amp;gt;&lt;/span&gt;&lt;span&gt; redirect&lt;/span&gt;&lt;span&gt;(to: &lt;/span&gt;&lt;span&gt;Routes&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;post_path&lt;/span&gt;&lt;span&gt;(conn, :index))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      {:error, changeset} &lt;/span&gt;&lt;span&gt;-&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        |&amp;gt;&lt;/span&gt;&lt;span&gt; put_flash&lt;/span&gt;&lt;span&gt;(:error, &lt;/span&gt;&lt;span&gt;&quot;failure couldn&apos;t create post&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        |&amp;gt;&lt;/span&gt;&lt;span&gt; render&lt;/span&gt;&lt;span&gt;(:new, changeset: changeset)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Jump to&lt;/em&gt; &lt;a href=&quot;#add-post-view-templates&quot;&gt;&lt;em&gt;Post View &amp;amp; Templates&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Breakdown&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  defmodule PhoenixBlogWeb.PostController do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    ...&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Controllers are just modules in Elixir. The module name (&lt;code&gt;PostController&lt;/code&gt;) needs to match the filename &lt;code&gt;post_controller.ex&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    use PhoenixBlogWeb, :controller&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;use&lt;/code&gt; will require and inject the &lt;code&gt;Phoenix.Controller&lt;/code&gt; code and the &lt;code&gt;Plug.Conn&lt;/code&gt; into our Post Controller. It does this by calling the macro &lt;code&gt;__using()__&lt;/code&gt; . You can see the code injected &lt;a href=&quot;https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/controller.ex#L165&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    alias PhoenixBlog.{&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      Post,&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      Repo&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Alias&lt;/code&gt; is used to define shortcuts. Instead of writing &lt;code&gt;PhoenixBlog.Post&lt;/code&gt; we can instead write &lt;code&gt;Post&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    def index(conn, _params) do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      posts = Repo.all(Post)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      render(conn, &quot;index.html&quot;, posts: posts)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;index&lt;/code&gt; method takes in a &lt;code&gt;conn&lt;/code&gt; and a &lt;code&gt;params&lt;/code&gt; argument. &lt;code&gt;conn&lt;/code&gt; comes from &lt;a href=&quot;https://hexdocs.pm/plug/Plug.Conn.html&quot; target=&quot;_blank&quot;&gt;Plug.Conn&lt;/a&gt;. Which was injected above via &lt;code&gt;use&lt;/code&gt;. &lt;code&gt;conn&lt;/code&gt; is a module that defines low level functions such as request information. Next is &lt;code&gt;params&lt;/code&gt;, we will prepend it with &lt;code&gt;_&lt;/code&gt; to signify that we aren’t using it. We then call &lt;code&gt;Repo.all(Post)&lt;/code&gt; to pull all the posts from the database. These posts are assigned to a variable which we can access in the index view via &lt;code&gt;@posts&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    def new(conn, _params) do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      changeset = Post.changeset(%Post{}, %{})&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      render(conn, &quot;new.html&quot;, changeset: changeset)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;new&lt;/code&gt; method creates a &lt;code&gt;changeset&lt;/code&gt; that is passed to the new view. A changeset in Elixir will only allow data with specific information to go through. It will also complain if it expects data but doesn’t receive it. &lt;code&gt;Post.changeset/2&lt;/code&gt; method takes in a Post struct and some attributes. Since we’re creating a new Post, there are no attributes so instead we’ll pass in an empty map.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    def create(conn, %{&quot;post&quot; =&amp;gt; post_params} = _params) do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      changeset = Post.changeset(%Post{}, post_params)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      case Repo.insert(changeset) do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {:ok, _log} -&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          |&amp;gt; put_flash(:info, &quot;Success - created a Post!&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          |&amp;gt; redirect(to: Routes.post_path(conn, :index))&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        {:error, changeset} -&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          |&amp;gt; put_flash(:error, &quot;failure couldn&apos;t create post&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;          |&amp;gt; render(:new, changeset: changeset)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The create method is pretty big so let’s break this down to smaller parts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    ...%{&quot;post&quot; =&amp;gt; post_params} = _params&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line of the method uses Elixir’s pattern matching to assign anything found in &lt;code&gt;params[&quot;post&quot;]&lt;/code&gt; to &lt;code&gt;post_params&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    changeset = Post.changeset(%Post{}, post_params)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’ve seen &lt;code&gt;changeset&lt;/code&gt; being used for the &lt;code&gt;new&lt;/code&gt; route. In the &lt;code&gt;create&lt;/code&gt; route we’re passing in the &lt;code&gt;post_params&lt;/code&gt; instead of an empty map. This creates a changeset that is populated by the form data. Which we’ll save to the database via:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    Repo.insert(changeset)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Repo.insert/1&lt;/code&gt; will return one of two tuples.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    {:ok, _log} -&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      |&amp;gt; put_flash(:info, &quot;Success - created a Post!&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      |&amp;gt; redirect(to: Routes.post_path(conn, :index))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On a successful insert into the database, it will display a success flash message and redirect to the index page where all the posts will be displayed.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    {:error, changeset} -&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      conn&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      |&amp;gt; put_flash(:error, &quot;failure couldn&apos;t create post&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      |&amp;gt; render(:new, changeset: changeset)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On an error inserting into the database, it will display an error flash message and re-render &lt;code&gt;new.html.eex&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Add Post View &amp;amp; Templates&lt;/h2&gt;
&lt;p&gt;Phoenix’s has a strong naming convention, which will link templates in the &lt;code&gt;post&lt;/code&gt; directory and the view, &lt;code&gt;post_view&lt;/code&gt;, to the Post Controller.&lt;/p&gt;
&lt;p&gt;Templates in Phoenix are written in &lt;code&gt;eex&lt;/code&gt; which stands for Embedded Elixir. They are the building blocks to a webpage. When we start the Phoenix server, templates get rendered into the view and displayed as html.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/views/post_view.ex&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    defmodule PhoenixBlogWeb.PostView do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      use PhoenixBlogWeb, :view&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/templates/index.html.eex.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h1&amp;gt;Blog Post&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= link &quot;+ Add new post&quot;, to: Routes.post_path(@conn, :new) %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= for post &amp;lt;- @posts do %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h2&amp;gt;&amp;lt;%= post.title %&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;p&amp;gt;&amp;lt;%= post.content%&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/templates/post/new.html.eex&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h1&amp;gt;New Blog Post&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= form_for @changeset, Routes.post_path(@conn, :create), fn f -&amp;gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        title:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;/label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= text_input f, :title %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        Content:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;/label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= textarea f, :content %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= submit &quot;Submit!&quot; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Breakdown&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/views/post_view.ex&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    defmodule PhoenixBlogWeb.PostView do&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      use PhoenixBlogWeb, :view&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The Post view has a similar structure to the Post controller. &lt;code&gt;use&lt;/code&gt; will inject the &lt;a href=&quot;https://github.com/phoenixframework/phoenix/blob/master/lib/phoenix/view.ex#L148&quot; target=&quot;_blank&quot;&gt;Phoenix.View code&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/templates/index.html.eex.&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h1&amp;gt;Blog Post&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= link &quot;+ Add new post&quot;, to: Routes.post_path(@conn, :new) %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= for post &amp;lt;- @posts do %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h2&amp;gt;&amp;lt;%= post.title %&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;p&amp;gt;&amp;lt;%= post.content%&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The index view will add a &lt;a href=&quot;https://hexdocs.pm/phoenix_html/Phoenix.HTML.Link.html&quot; target=&quot;_blank&quot;&gt;link&lt;/a&gt; that routes to the new Post path. It will also loop through all the &lt;code&gt;posts&lt;/code&gt; and display the title and content. The &lt;code&gt;@posts&lt;/code&gt; variable was set in the PostController index method.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    # lib/phoenix_blog_web/templates/post/new.html.eex&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;h1&amp;gt;New Blog Post&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;%= form_for @changeset, Routes.post_path(@conn, :create), fn f -&amp;gt; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        title:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;/label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= text_input f, :title %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        Content:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;/label&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= textarea f, :content %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;      &amp;lt;%= submit &quot;Submit!&quot; %&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    &amp;lt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The form inside &lt;code&gt;new.html&lt;/code&gt; will use the &lt;code&gt;changeset&lt;/code&gt; variable we set in the PostController new method. If you want more information on the types of input fields, see the &lt;a href=&quot;https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html&quot; target=&quot;_blank&quot;&gt;Phoenix docs&lt;/a&gt;. When the user clicks on submit, the form data gets sent to the PostController create method via &lt;code&gt;Routes.post_path(@conn, :create)&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Phew, we finally made it! Now if you start your webserver:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;    mix phx.server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Navigate to &lt;code&gt;localhost:4000/posts&lt;/code&gt; you should see &lt;code&gt;Blog Post&lt;/code&gt; as a header. If you click on the link or go to &lt;code&gt;localhost:4000/posts/new&lt;/code&gt; you should see a form to create a post.&lt;/p&gt;
&lt;p&gt;Try implementing the &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt; route next! You can apply the patterns in &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt; to help guide you. I hope this tutorial has highlighted how easy it is to get started with Phoenix. If you enjoyed this article please leave a comment below. Happy coding!&lt;/p&gt;
&lt;div&gt;
	&lt;hr /&gt;
	&lt;p&gt;
		Thanks for subscribing to my RSS feed! If you have any thoughts or feedback on this post, send me an
		&lt;a href=&quot;mailto:hey@jonathanyeong.com&quot;&gt;email&lt;/a&gt; or DM me on &lt;a href=&quot;https://bsky.app/profile/jonathanyeong.com&quot; rel=&quot;noopener noreferrer&quot; target=&quot;_blank&quot;&gt;Bluesky&lt;/a&gt;.
	&lt;/p&gt;
&lt;/div&gt;
</content:encoded></item></channel></rss>