<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[wojtek.im]]></title>
        <description><![CDATA[Posts about user interfaces, design, tech, and more, from Wojtek Witkowski (@pugson)]]></description>
        <link>https://wojtek.im/journal</link>
        <image>
            <url>https://wojtek.im/face.jpg</url>
            <title>wojtek.im</title>
            <link>https://wojtek.im/journal</link>
        </image>
        <generator>wojtek.im</generator>
        <lastBuildDate>Sat, 14 Mar 2026 09:30:12 GMT</lastBuildDate>
        <atom:link href="https://wojtek.im/rss.xml" rel="self" type="application/rss+xml"/>
        <language><![CDATA[en]]></language>
        <item>
            <title><![CDATA[You need PgBouncer when deploying Postgres on Railway]]></title>
            <description><![CDATA[Don't let your app suddenly break when connections are maxed out and you see 'FATAL:  sorry, too many clients already' in the logs.]]></description>
            <link>https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients</link>
            <guid isPermaLink="true">https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients</guid>
            <pubDate>Tue, 03 Mar 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients">https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients</a>.</em></p>

<p><div data-language="shell" data-code="2026-03-02 22:12:19.982 UTC [56699] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:20.459 UTC [56700] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:22.046 UTC [56701] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:28.046 UTC [56702] FATAL:  sorry, too many clients already"><code-block data-language="shell">2026-03-02 22:12:19.982 UTC [56699] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:20.459 UTC [56700] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:22.046 UTC [56701] FATAL:  sorry, too many clients already<br />
2026-03-02 22:12:28.046 UTC [56702] FATAL:  sorry, too many clients already</code-block></div><p>Looks familiar?</p><p></p><p>This can happen to your app and your users can lose work because of maxed out connections to Postgres.</p><p></p><p><a target="_blank" rel="noopener noreferrer nofollow" href="https://railway.com/?referralCode=dev">Railway</a> does not let you manually edit the <code>max_connections</code> config anywhere in their dashboard. Probably for the better, since this would be a bandaid and not a proper fix.</p><p></p><p>This is where <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/pgbouncer/pgbouncer">PgBouncer</a> comes in.</p><img src="https://wojtek-im-cms.t3.storage.dev/uploads/0ROKfOovmYIiGfcs1eWOs.png" alt="CleanShot 2026-03-03 at 12.25.11.AM@2x.png" data-name="CleanShot 2026-03-03 at 12.25.11.AM@2x.png" data-url="https://wojtek-im-cms.t3.storage.dev/uploads/0ROKfOovmYIiGfcs1eWOs.png"><p>Install the template made by Brody and it will automatically pre-fill all the necessary Postgres variables from your Railway project.</p><p></p><p>If you have just one database in that project, then it's seamless. If you have more than one then you're gonna need to manually check / modify the template's variables before deploying.</p><p></p><p>After you are done deploying the template, go ahead and create a public domain for <strong>PgBouncer</strong> in the settings. After that is done update your app's environment variables to use the new <code>DATABASE_PUBLIC_URL</code> from the <strong>PgBouncer</strong> service which you can easily copy from the <em>Variables</em> screen.</p><p></p><starbox><div class="bn-inline-content">Important:&nbsp;If you are using an ORM such as Drizzle, you will need to keep the direct Postgres DB configuration for migrations. Give it a name of <code>DATABASE_URL_DIRECT</code>&nbsp;and point your ORM config directly to the database.</div></starbox><h2 data-level="2">Optimizing for serverless</h2><p>For an app that uses Next.js, <strong>transaction</strong> mode is the right choice — it returns connections to the pool after each transaction, so they're shared much more efficiently across serverless functions and Server Actions deployed on Vercel.</p><p></p><p>You can set <code>PGBOUNCER_POOL_MODE</code> to <code>transaction</code> to enable this.</p><p></p><p>Just make sure you disable prepared statements in your ORM of choice because PgBouncer can route your next query to a different backend connection that doesn't have the prepared statement and cause issues.</p><p></p><p>Drizzle, which runs on <code>node-postgres</code> under the hood, does not use prepared statements so you are good to go out of the box.</p><p></p><p>More reading about different pooling modes here:</p><div data-url="https://jpcamara.com/2023/04/12/pgbouncer-is-useful.html" data-no-image="true" data-fallback-title="PgBouncer is useful, important, and fraught with peril" data-fallback-description="Updated 2024-09-17 to reflect updated PgBouncer support for protocol-level prepared statements 🐘  To start, I want to say that I’m appreciative that PgBouncer exists and the work its open source maintainers put into it. I also love working with PostgreSQL, and I’m thankful for the incredible amount of work and improvements that go into it as well. I also think community and industry enthusiasm around Postgres is at an all time high."><link-embed data-url="https://jpcamara.com/2023/04/12/pgbouncer-is-useful.html" data-no-image="true" data-fallback-title="PgBouncer is useful, important, and fraught with peril" data-fallback-description="Updated 2024-09-17 to reflect updated PgBouncer support for protocol-level prepared statements 🐘  To start, I want to say that I’m appreciative that PgBouncer exists and the work its open source maintainers put into it. I also love working with PostgreSQL, and I’m thankful for the incredible amount of work and improvements that go into it as well. I also think community and industry enthusiasm around Postgres is at an all time high."></link-embed></div><p></p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s stopping you now?]]></title>
            <description><![CDATA[AI coding is the best thing that happened to people with ADHD and executive dysfunction. ]]></description>
            <link>https://wojtek.im/journal/whats-stopping-you-now</link>
            <guid isPermaLink="true">https://wojtek.im/journal/whats-stopping-you-now</guid>
            <pubDate>Wed, 11 Feb 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/whats-stopping-you-now">https://wojtek.im/journal/whats-stopping-you-now</a>.</em></p>

<p>Remember that to-do list from 2 years ago? Those tasks that seemed impossible to do. If only “you had more time” to focus. </p><p></p><p>I spent years staring at my to-dos that would never materialize. They were taking up precious cognitive load and preventing me from taking action on other things. Just a single line with a few words causing distress. A task so simple but so out of reach.</p><p></p><p>I wanted to integrate a simple CMS for this website and I finally did. But there was one key feature that wasn’t fully aligned with my mental model. I sent an email to the founder asking if there was a way to send a webhook when a new post was created.</p><p></p><p>Instead of waiting around I ripped out the service I just integrated 15 minutes earlier and told Claude to build a custom solution tailored to my needs. </p><p></p><p>By the time I got a reply I had a fully fleshed out custom CMS integrated into my website with even more features. </p><p>Of course the answer from the founder was "no".</p><p></p><p>I'm writing this post using the rich editor Claude built.</p><img src="https://wojtek-im-cms.t3.storage.dev/uploads/aRVdom2uvj11Db4FmWuA7.png" alt="CleanShot 2026-02-11 at 03.16.24.PM@2x.png" data-name="CleanShot 2026-02-11 at 03.16.24.PM@2x.png" data-url="https://wojtek-im-cms.t3.storage.dev/uploads/aRVdom2uvj11Db4FmWuA7.png"><p></p><p>If you can’t spare 8 hours each day to get a big project done, do something small and see the compounding effects later.</p><p></p><p>Get unstuck. Gain momentum. Even if it's not good enough, <strong>it's much easier to iterate on something that exists</strong> than starting off with a blank slate.</p><p> </p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[You should build it anyway]]></title>
            <description><![CDATA[Software that works with your mental model is unmatched. That’s why there can be an infinite amount of the same thing.]]></description>
            <link>https://wojtek.im/journal/you-should-build-it-anyway</link>
            <guid isPermaLink="true">https://wojtek.im/journal/you-should-build-it-anyway</guid>
            <pubDate>Wed, 14 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/you-should-build-it-anyway">https://wojtek.im/journal/you-should-build-it-anyway</a>.</em></p>

<p>The mantra of <em>"Why would you make this when [thing] exists already?"</em> is played out.</p><p></p><p>With the rise of AI tools like <a target="_blank" rel="noopener noreferrer nofollow" href="https://claude.com/product/claude-code">Claude Code</a> and <a target="_blank" rel="noopener noreferrer nofollow" href="https://cursor.com">Cursor</a> there is no excuse not to build something you want, exactly how you want it.</p><p></p><p>People love to laugh and complain about yet another to-do app but they all have a reason for existing. </p><starbox><div class="bn-inline-content">There could be a million apps that do the same thing but if your mental model is not aligned with the app then there might as well be zero. Keep building.</div></starbox><ul><li><p class="bn-inline-content">High subscription price? Build your own.</p></li><li><p class="bn-inline-content">Don't like the design? Build your own.</p></li><li><p class="bn-inline-content">Lack of specific features? Build your own.</p></li><li><p class="bn-inline-content">Scared it might shut down in a year? Build your own.</p></li></ul><p></p><p>So many recipes for making the same meal exist. Some people only want it done a specific way. Others are fine with whatever as long as it has the same shape and form. </p><p></p><p>Those who end up cooking something that tastes great to the masses are able to turn it into a business. </p><p></p><p>After all, the cheeseburger from McDonald's did not start as an item eaten by billions. Two brothers had to make a burger they liked first.</p><p></p><p>Focus on what you want and what works well with your mental model. The hypergrowth mindset will only kill your desire to make something good and have fun while doing it.</p><h2 data-level="2">Software for one+</h2><p>Build it for yourself first then share it with friends.</p><p></p><p>For some people your app's mental model might fit their own and they'll love it. Maybe they will give you some useful suggestions on how to make this even better for both of you. </p><p></p><p><em>(This is what happened with my iOS app </em><em><a target="_blank" rel="noopener noreferrer nofollow" href="https://pixeldrop.app">Pixeldrop</a></em><em>)</em></p><p></p><p>But if they don't, they can create their own version because there's nothing stopping them now.</p><h2 data-level="2">Use existing systems</h2><p>Sometimes you don't even need to build a full app, you can just use an existing API and create something small that fixes what the original product lacked for you to be fully satisfied.</p><p></p><p>I use an <a target="_blank" rel="noopener noreferrer nofollow" href="https://ouraring.com/">Oura Ring</a> for sleep tracking but I didn't like how my sleep pattern data was displayed in the Oura app. </p><p></p><p>Looking around the web for solutions produced results that were either too much effort to set up or <strong>did not match my mental model</strong> of what data I wanted to focus on.</p><p></p><p>So instead I found the <a target="_blank" rel="noopener noreferrer nofollow" href="https://cloud.ouraring.com/docs/">Oura API</a> and looked at what I can extract from the responses. This kicked off a custom interactive dashboard with my sleep data.</p><img src="https://wojtek-im-cms.t3.storage.dev/uploads/mrSWRNiWf-DFNnJB0Xwl3.png" alt="CleanShot 2026-01-14 at 07.23.55.PM@2x.png" data-name="CleanShot 2026-01-14 at 07.23.55.PM@2x.png" data-url="https://wojtek-im-cms.t3.storage.dev/uploads/mrSWRNiWf-DFNnJB0Xwl3.png"><p>I published it online and sent a link to my friends. This tool ended up being super helpful because now they bully me to go to sleep earlier if I'm still up sending messages.</p><p></p><p><strong>Start small and get it done. You might be surprised how fun it is to solve your own problems with software tailored exactly to your needs.</strong></p><hr><p>If you need an affordable and performant way to host your app, bot, or microservice I can recommend using <a target="_blank" rel="noopener noreferrer nofollow" href="https://railway.com/?referralCode=dev">Railway</a>.</p><p></p><p>If you haven't tried Claude Code yet, you can use my guest pass and get a free week by <a target="_blank" rel="noopener noreferrer nofollow" href="https://claude.ai/referral/Y2ykrV9zZg">using this link</a>.</p><p></p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Autoplay does not work on Mobile Safari in Low Power Mode]]></title>
            <description><![CDATA[TLDR: There is no way to fix this. Give up.]]></description>
            <link>https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode</link>
            <guid isPermaLink="true">https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode</guid>
            <pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode">https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode</a>.</em></p>

<p>You probably ran into a bug where your video that had <code>muted</code>, <code>loop</code>, <code>playsinline</code>, <code>autoplay</code> properties set was not playing and had a giant play button overlay on it when using Safari.</p><p></p><p>This happens due to Low Power Mode. Apple prevents automatic playback of any video with LPM enabled.</p><p></p><p>Unfortunately <strong>there is no way around it</strong> other than getting the user to press the native play button or a custom one that calls the <code>play()</code> function on the video element.</p><h2 data-level="2">No way to detect it</h2><p>iOS does not expose a way to detect if Low Power Mode is enabled to prevent fingerprinting risk when websites track you.</p><p></p><h2 data-level="2">Three.js and React Three Fiber also affected</h2><p>With Low Power Mode enabled, you will not be able to autoplay a video plane inside a Three.js scene either without user interaction. </p><h2 data-level="2">WebP workaround for short videos</h2><p>If this is a tiny decorative video there is a possible way to work around this issue. Safari treats animated images differently than videos and lets them autoplay without any restrictions in LPM.</p><starbox><div class="bn-inline-content">Only supported in Safari 16 or newer. Safari 14 and 15 have partial support but you might encounter issues.</div></starbox><p>You're going to need to use <code>ffmpeg</code> to convert your video into WebP first.<br></p><div data-language="bash" data-code="ffmpeg -i VIDEO_FILE.mp4 -vcodec libwebp -lossless 0 -q:v 80 -loop 0 -an OUTPUT.webp"><code-block data-language="bash">ffmpeg -i VIDEO_FILE.mp4 -vcodec libwebp -lossless 0 -q:v 80 -loop 0 -an OUTPUT.webp</code-block></div><p>Then set up a component that loads up the video and checks if it can be played. If playback fails then hide the video element and show an <code>&lt;img&gt;</code> with a WebP source in its place.</p><p>However, <strong>I truly do not recommend this method</strong> as the WebP file will most likely be much larger than the original video and take longer to load + use even more resources.</p><p>Easier to just let go and give up. You win this one, Tim Apple. 😩<br></p><p></p><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Favorite Personal Sites of 2025]]></title>
            <description><![CDATA[Hand-picked collection of pretty cool websites.]]></description>
            <link>https://wojtek.im/journal/favorite-personal-sites-of-2025</link>
            <guid isPermaLink="true">https://wojtek.im/journal/favorite-personal-sites-of-2025</guid>
            <pubDate>Fri, 09 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/favorite-personal-sites-of-2025">https://wojtek.im/journal/favorite-personal-sites-of-2025</a>.</em></p>

<p>It's always good to have a place online that you can call home and experiment with. These sites are all full of personality and let you get a glimpse of what goes on in the mind of their creators.</p><p></p><p>I'm just going to drop the links without saying what I like about each site. You can <strong>form your own unbiased opinion</strong>.</p><p></p><p>Hope these inspire you. Some might surprise you.</p><p>Presented in no particular order.</p><div data-url="https://austinvalleskey.com" data-compact="true" data-fallback-title="Austin Valleskey"><link-embed data-url="https://austinvalleskey.com" data-compact="true" data-fallback-title="Austin Valleskey"></link-embed></div><div data-url="https://keyavadgama.com" data-compact="true" data-fallback-title="Keya Vadgama"><link-embed data-url="https://keyavadgama.com" data-compact="true" data-fallback-title="Keya Vadgama"></link-embed></div><div data-url="https://edmundo.is" data-compact="true"><link-embed data-url="https://edmundo.is" data-compact="true"></link-embed></div><div data-url="https://www.raffi.zip" data-compact="true"><link-embed data-url="https://www.raffi.zip" data-compact="true"></link-embed></div><div data-url="https://emilkowal.ski" data-compact="true"><link-embed data-url="https://emilkowal.ski" data-compact="true"></link-embed></div><div data-url="https://expensive.toys" data-no-image="true"><link-embed data-url="https://expensive.toys" data-no-image="true"></link-embed></div><div data-url="https://www.jeremygoldberg.xyz" data-compact="true"><link-embed data-url="https://www.jeremygoldberg.xyz" data-compact="true"></link-embed></div><div data-url="https://glmb.today" data-no-image="true"><link-embed data-url="https://glmb.today" data-no-image="true"></link-embed></div><div data-url="https://consumed.today" data-compact="true"><link-embed data-url="https://consumed.today" data-compact="true"></link-embed></div><div data-url="https://miguel.build/" data-compact="true"><link-embed data-url="https://miguel.build/" data-compact="true"></link-embed></div><div data-url="https://webb.page" data-compact="true"><link-embed data-url="https://webb.page" data-compact="true"></link-embed></div><div data-url="https://benji.org" data-compact="true"><link-embed data-url="https://benji.org" data-compact="true"></link-embed></div><div data-url="https://nelson.co" data-compact="true"><link-embed data-url="https://nelson.co" data-compact="true"></link-embed></div><div data-url="https://www.uilabs.dev" data-compact="true"><link-embed data-url="https://www.uilabs.dev" data-compact="true"></link-embed></div><div data-url="https://chmiel.work" data-compact="true"><link-embed data-url="https://chmiel.work" data-compact="true"></link-embed></div><div data-url="https://www.abjt.dev" data-compact="true"><link-embed data-url="https://www.abjt.dev" data-compact="true"></link-embed></div><div data-url="https://gregsarafian.com" data-compact="true"><link-embed data-url="https://gregsarafian.com" data-compact="true"></link-embed></div><div data-url="https://adamwhitcroft.com" data-compact="true" data-fallback-title="Adam Whitcroft" data-fallback-description="Software designer from South Africa, living in Canada." data-fallback-image-url="https://adamwhitcroft.com/assets/icons/mapkin.png"><link-embed data-url="https://adamwhitcroft.com" data-compact="true" data-fallback-title="Adam Whitcroft" data-fallback-description="Software designer from South Africa, living in Canada." data-fallback-image-url="https://adamwhitcroft.com/assets/icons/mapkin.png"></link-embed></div><div data-url="https://toddwilson.studio" data-compact="true"><link-embed data-url="https://toddwilson.studio" data-compact="true"></link-embed></div><div data-url="https://kuldar.com" data-compact="true"><link-embed data-url="https://kuldar.com" data-compact="true"></link-embed></div><p></p><starbox><div class="bn-inline-content">If you were on the fence about creating your own personal site, just do it. It has compounding effects.</div></starbox><p></p><p>But please <strong>make it your own</strong> and not just a blatant copy of someone else's website. <strong>No stealing</strong> and pretending you had an original idea without giving credit, okay?</p><hr><p>Want to include rich link previews like these on your own website?</p><div data-url="https://wojtek.im/journal/creating-a-link-embed-react-server-component" data-no-video="true"><link-embed data-url="https://wojtek.im/journal/creating-a-link-embed-react-server-component" data-no-video="true"></link-embed></div><p></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to target Safari with a CSS @supports media query]]></title>
            <description><![CDATA[Easiest method for targeting Safari with CSS and Tailwind in 2026.]]></description>
            <link>https://wojtek.im/journal/targeting-safari-with-css-media-query</link>
            <guid isPermaLink="true">https://wojtek.im/journal/targeting-safari-with-css-media-query</guid>
            <pubDate>Sat, 03 Jan 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/targeting-safari-with-css-media-query">https://wojtek.im/journal/targeting-safari-with-css-media-query</a>.</em></p>

<blockquote>
  Yes, this also works with Safari on macOS 26 Tahoe, iOS 26, iPadOS
  26, and visionOS 26.
</blockquote>

<p>I just spent over 30 minutes looking for a way to only target Safari with CSS media queries and could not find a reliable way that works with Safari without also targeting Chrome.</p>

<p>Looking through the compatibility table on <a href="https://caniuse.com/css-hanging-punctuation">caniuse.com</a> you can spot that Safari 16 and above has a unique property which is not supported by Chrome or Firefox called <code>hanging-punctuation</code>.</p>

<p>Using that property and <code>-webkit-appearance</code> we can target Safari specifically until any other browser decides to support it.</p>

<pre><code>@supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none) {
  .safari-only {
    background-color: red;
  }
}
</code></pre>

<p>
  ✦ 2024 Update: You can also chain it with <code>font: -apple-system-body</code> to make
  this more bulletproof. Thanks to <a href="https://soberia.ir/?ref=wojtek.im">Saber
  Hayati</a> for the tip.
</p>

<h2>Demo</h2>

<p>If you open this page in Safari, the right column will be red.</p>

<p><div className="flex h-32 items-center justify-center rounded-2xl border border-border bg-white/[3.5%] p-4 text-center text-white"><br />
    All Browsers<br />
  <br />
  <br />
    Safari only<br />
  <br />
</div></p>

<h2>Using this method with Tailwind CSS v3</h2>

<p>You don't have to be limited to just CSS. Extending your Tailwind config with a custom plugin that adds a <code>safari-only:</code> variant to your Tailwind config is pretty easy:</p>

<pre><code>module.exports = {
  theme: {
    // ...
  },
  plugins: [
    function({ addVariant }) {
      addVariant('safari-only', '@supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none)');
    },
  ],
};

</code></pre>

<h2>Using this method with Tailwind CSS v4</h2>

<p>Tailwind v4 uses a CSS-native <code>@custom-variant</code> directive instead of JavaScript plugins. Add this to your main CSS file (e.g., <code>app.css</code> or <code>globals.css</code>):</p>

<pre><code>@custom-variant safari-only {
  @supports (hanging-punctuation: first) and (font: -apple-system-body) and (-webkit-appearance: none) {
    @slot;
  }
}
</code></pre>

<p>The <code>@slot</code> marker indicates where the utility styles get inserted. Usage is the same as in v3:</p>

<pre><code><div class="safari-only:bg-red-500">Safari only</div>
</code></pre>

<h3>Nice, but why does it also target Arc, Chrome, Firefox, Brave, Internet Explorer, etc. on my iPhone, iPad, or Vision Pro?</h3>

<p>Because iOS and iPadOS (and visionOS) use the Mobile Safari rendering engine for all webviews. This means if you are using Chrome or any other “browser” on your iPhone it is still Safari under the hood.</p>

<p>That’s the case until these apps ship with their own rendering engines which will start being <a href="https://www.theverge.com/2024/1/25/24050478/apple-ios-17-4-browser-engines-eu">possible with iOS 17.4 for the EU.</a></p>

]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Keychron M5 Vertical Mouse as a Mac user]]></title>
            <description><![CDATA[`Is this better than the Logitech MX Vertical?`]]></description>
            <link>https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac</link>
            <guid isPermaLink="true">https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac</guid>
            <pubDate>Sun, 05 Oct 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac">https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac</a>.</em></p>

<p>I keep searching for a perfect workspace mouse to use with my Mac. One that will make me finally forget about the <strong>Logitech MX Vertical</strong> which is the gold standard of ergonomics, in my eyes.</p>

<p>Unfortunately, it doesn't seem like Logitech will be releasing a work mouse with polling rate over 125Hz anytime soon based on the brand new <a href="https://www.engadget.com/computing/accessories/the-logitech-mx-master-4-is-here-with-haptic-feedback-less-rubber-and-the-same-shape-070129314.html">MX Master 4</a> that came out this month.</p>

<p><a href="/journal/razer-pro-click-v2-vertical-review">Razer Pro Click V2 Vertical was awful</a> and I returned it.</p>

<p>Then a few months later reviews for the Keychron M5 Vertical showed up on the internet, so naturally I had to order it to try it out for myself. Pricing is <strong>$70 in the US</strong> and around <strong>$95 in Europe</strong>.</p>

<p>I decided to pick up the white version because it might hide the wear and tear a bit better down the line.</p>

<img src="https://cdn.shopify.com/s/files/1/0059/0630/1017/files/M5-8K-7.jpg?v=1743646905" width="1140" height="640" alt="Keychron M5 Vertical" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<h2>Polling Rate</h2>

<p>This mouse can go <strong>up to 8000Hz polling rate</strong> using the 2.4GHz dongle. Honestly, that is way overkill for your standard work setup and will drain the battery faster for zero perceivable benefit.</p>

<p>But we love to see it. Finally someone taking the polling rate seriously. It's buttery smooth.</p>

<p>I'm keeping mine at 2000Hz at 2800 DPI which I think is a sweet spot for battery life and responsiveness.</p>

<h2>Battery Life</h2>

<p>With the settings above, the mouse lasted a full month on a single charge with pretty much daily 6-12 hours of use. No complaints here. That's good enough for me.</p>

<h2>Software</h2>

<p>Keychron has their own app for configuring the mouse and <a href="https://launcher.keychron.com/#/mouse/dpi">it's a web app</a>! Which means no bloated software to install, no updates to worry about, and you can configure it from any device.</p>

<p>You can remap buttons to key combos like <code>⌘W</code> natively, which is great.</p>

<p>Your configuration is saved to the onboard memory so you can easily switch between devices without having to think about software or configuration.</p>

<p>However there is no option to change the sensitivity of both scroll wheels. The default sensitivity of the horizontal wheel is too low. While the vertical wheel's scroll speed is a tad too fast for me.</p>

<p>That can be easily solved with <a href="https://plentycom.jp/en/steermouse/">SteerMouse</a> but I decided to keep it simple and use default settings while I'm still getting used to this mouse.</p>

<img src="https://labs-web-static-assets.s3.wasabisys.com/product-admin/1757092419065-hardwareConnectivityAssets-1.jpg" width="1200" height="640" alt="Keychron M5 Vertical bottom view" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />
<p>
  Photo courtesy of <a href="https://www.lttlabs.com/products/mice/keychron-m5-wireless-ergonomic-mouse">LTT Labs</a>
</p>

<h2>Comfort</h2>

<p>I was hoping this would be almost a clone of the MX Vertical in shape and angle but it's not. The mouse sits lower and offers less support on the palm and for your thumb. I end up dragging my pinky on the desk while using it. There is also a chance of pinching it between the mouse and the desk.</p>

<p>Maybe I should 3D print a custom pinky shelf to prevent this from happening?</p>

<p>Here are a few other gripes:</p>

<li><strong>Not enough weight.</strong> Feels a bit too light. I would prefer just a tiny bit more substance to get rid of that cheap plastic feel. </li>
<li><strong>Not enough grip.</strong> The plastic is too slippery and my thumb ends up sliding too much. You cannot easily pick the mouse up while grabbing it. I need to add some grip tape. Maybe the finish will wear out over time and become less slippery?</li>
<li><strong>Horizontal scroll wheel is too close to the side buttons.</strong> Sometimes I end up ghost clicking the side buttons when scrolling the wheel down. And when one of them is mapped to close the current tab or window, that's not exactly fun.</li>

<img src="https://cdn.shopify.com/s/files/1/0824/6824/1739/files/imgi_309_47degree-vertical-angle-of-M5-8K.png?v=1750079099" width="600" height="600" alt="Keychron M5 Vertical angle" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />
<img src="https://resource.logitech.com/w_776,h_437,ar_16:9,c_fill,q_auto,f_auto,dpr_1.0/d_transparent.gif/content/dam/logitech/en/products/mice/mx-vertical/mx-vertical-advance-ergonomics-02.png" width="600" height="600" alt="Keychron M5 Vertical angle" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>
  Keychron M5 Vertical (left) has an angle of 47° while MX Vertical (right) has a more comfortable 57°
</p>

<h2>Conclusion</h2>

<img src="https://labs-web-static-assets.s3.wasabisys.com/product-admin/1757092439346-thumbnail_image.jpg" width="1200" height="640" alt="Keychron M5 Vertical packaging" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />
<p>
  Photo courtesy of <a href="https://www.lttlabs.com/products/mice/keychron-m5-wireless-ergonomic-mouse">LTT Labs</a>
</p>

<p>Keychron M5 Vertical is a great contender in the vertical mouse market. Excellent polling rate, decent build quality, and a non-invasive software experience.</p>

<p>However, it is not perfect.</p>

<p>The comfort could be better by lifting the wrist a bit higher off the table. The grip is a bit too slippery and the angle is not as comfortable as the MX Vertical.</p>

<blockquote>
If the MX Vertical stopped existing tomorrow, I would be happy with the Keychron M5 Vertical.
</blockquote>

<p>So far I spent around 3 weeks of regular use with the Keychron. Going to use it as my daily driver for the forseeable future.</p>

<p>I really wish these companies started including a USB-C wireless dongle by default with an extra USB-C to USB-A adapter.</p>

<p>Why is this backwards?</p>

<p>You have to add extra bulk to the dongle to plug it into a MacBook with another adapter for no reason. I'm sure most PCs and laptops have a USB-C port these days.</p>

<hr />
<p>
For full technical specs and a more detailed review, check out <a href="https://www.lttlabs.com/products/mice/keychron-m5-wireless-ergonomic-mouse">LTT Labs</a>.
</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Best React Native UI resources for creating beautiful apps]]></title>
            <description><![CDATA[Speed up your app development with these amazing components, libraries, and examples. Growing list of useful resources.]]></description>
            <link>https://wojtek.im/journal/best-react-native-ui-resources</link>
            <guid isPermaLink="true">https://wojtek.im/journal/best-react-native-ui-resources</guid>
            <pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/best-react-native-ui-resources">https://wojtek.im/journal/best-react-native-ui-resources</a>.</em></p>

<p>React Native does not have the best reputation when it comes to quality of user interfaces. People associate it with cheap, low-quality apps which is a <strong>misconception</strong>. Just look how many popular apps have been built with <a href="https://expo.dev">Expo</a> and React Native.</p>

<p>As a mobile engineer, you need to stay on top of gestures, animations, haptics, navigation, and render performance. Especially when you are competing with some of the best teams in the world using UIKit, like <a href="https://family.co">Family</a>.</p>

<p><strong>It all comes down to craft.</strong></p>

<p>You don't want to be porting vibe coded shadcn slop from web to mobile because it goes against the grain of the platform.</p>

<p>Putting in extra effort will pay off. Make your users feel like the app is an extension of their phone and not a foreign object.</p>

<p>Luckily, there are a lot of resources out there that can help you speed up the process of making a great app or teach you how to do it properly.</p>

<p>I've compiled a list of things that I keep coming back to.</p>

<hr />
<p>
  Some links on this page are affiliate links. If you purchase something using my referral code, I will
  get a small commission. I only recommend products that I have personally purchased and/or found useful.
</p>
<hr />

<h2><a href="https://animatereactnative.lemonsqueezy.com?aff=QjyNx">Animate React Native</a></h2>

<p>Over 100 animated components for your app using Reanimated, Gesture Handler, and Moti. Many patterns from popular apps that will make your interactions feel more native.</p>

<p>Give the <a href="https://apps.apple.com/app/animatereactnative/id6738016513">demo app</a> a spin and see for yourself.</p>

<h2><a href="https://www.makeitanimated.dev/">Make It Animated</a></h2>

<p>Incredibly detailed recreations of patterns from popular iOS apps like ChatGPT, Slack, Discord, Raycast, Instagram, GitHub, Twitter, Pinterest, and Linear.</p>

<p>You get full access to the source code and <a href="https://apps.apple.com/us/app/make-it-animated/id6742489722">even a mobile app</a> to try out the animations on your own devices.</p>

<h2><a href="https://www.reactnativecomponents.com">React Native Components</a></h2>

<p>There is a whole library of popular apps and their parts recreated in React Native with code that you can copy and paste into your own app. Previously named <strong>Landing Components</strong>.</p>

<h2><a href="https://reactiive.io/demos">Reactiive</a></h2>

<p>Lots of components and patterns you might need for a modern mobile app. More granular than Landing Components, but also very high quality. 110+ React Native animations<br />
with Skia, Reanimated and Gesture Handler.</p>

<p>They just went open source on September 23, 2025 and everything is free and available on GitHub.</p>

<h2><a href="https://reactnativeglow.com/">React Native Glow</a></h2>

<p>When someone tells you to "make it pop", you can start by adding animated gradient glows around your buttons and elements.</p>

<h2><a href="https://zeego.dev">Zeego</a></h2>

<p>The easiest way to get native context menus working on iOS and Android. Radix-style component API makes it straightforward to use. You have a lot of options to customize the menu to your liking.</p>

<p>Plus, on iOS you can always reach into <code>react-native-ios-context-menu</code> directly if you need.</p>

<h2><a href="https://github.com/nandorojo/galeria">Galeria</a></h2>

<p>Beautiful image gallery library for adding photo grids and zoomable images with natural gestures.</p>

<h2><a href="https://github.com/fbeccaceci/react-native-fast-squircle">react-native-fast-squircle</a></h2>

<p>Do you want to make your app feel even more native? Smooth out those buttons and elements with a squircle shape.</p>

<h2><a href="https://github.com/callstack/liquid-glass">callstack/liquid-glass</a></h2>

<p>Yes, a native implementation of Liquid Glass from iOS 26. Good luck doing this in Flutter.</p>

<h2><a href="https://www.spottedinprod.com/">Spotted in Prod</a></h2>

<p>Collection of best iOS apps shipped to production. You can find a lot of great patterns and animations here that can get you unstuck. Absorb and learn.</p>

<p>Tiny humblebrag but my app, <a href="https://pixeldrop.app">Pixeldrop</a>, is also <a href="https://www.spottedinprod.com/apps/pixeldrop">featured on SIP</a> 😇</p>

<p>---</p>

<p>Hope some of these resources end up being useful.</p>

<p>More to come.</p>

<p>Subscribe to the newsletter below to get notified when I publish updates to this list. Or follow the <a href="https://t.me/wojtekim">Telegram channel</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An expensive disappointment: Razer Pro Click V2 Vertical]]></title>
            <description><![CDATA[`3 weeks with the "ergonomic" Razer Vertical mouse as a Mac user.`]]></description>
            <link>https://wojtek.im/journal/razer-pro-click-v2-vertical-review</link>
            <guid isPermaLink="true">https://wojtek.im/journal/razer-pro-click-v2-vertical-review</guid>
            <pubDate>Mon, 26 May 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/razer-pro-click-v2-vertical-review">https://wojtek.im/journal/razer-pro-click-v2-vertical-review</a>.</em></p>

<p>I've been in search of a perfect workspace mouse for a long time.</p>

<p>Even considered trying to build my own out of parts from a Logitech gaming mouse with a <a href="https://www.logitechg.com/en-us/innovation/lightspeed.html">Lightspeed</a> sensor to fit into the <a href="https://www.logitech.com/en-eu/shop/p/mx-vertical-ergonomic-mouse.910-005448">MX Vertical</a> enclosure.</p>

<p>{/<em> Unfortunately the layout of the PCBs and scroll wheel placement would have been impossible to fit in there without soldering (which I know nothing about yet). FDM 3D printing would not match the quality of injection molding anyway. </em>/}</p>

<p>Some <a href="https://www.reddit.com/r/MouseReview/comments/1ie9z2i/logitech_mx_vertical_g_pro_x_superlight/">people on Reddit have attempted</a> to do this but the results are not ideal.</p>

<p>{/<em> I've gone through a lot of mice throughout the years but never landed on one that had it all. </em>/}</p>

<p>There was no vertical mouse with a high polling rate on the market until now.</p>

<h2>Meet the Razer Pro Click V2 Vertical</h2>

<img src="https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/eeb5eb2a-0cbb-4b52-7bf0-e0379d63f600/original" width="960" height="960" alt="Razer Pro Click V2 Vertical from two angles" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>A friend sent me a link to a YouTube video of someone unboxing and reviewing this thing. Of course the review was very positive — just like all the pre-release sponsored propaganda these tech channels put out on a regular basis. 🤢</p>

<p>I saw that it was wireless, vertical, and had a 1000Hz polling rate — exactly what I was looking for.</p>

<p>It was time to hit that <code>Buy Now</code> button on <a href="https://www.razer.com/eu-en/productivity/razer-pro-click-v2-vertical-edition">Razer's website</a> and try it out for myself.</p>

<p>The price was a whopping €129.99 ($150) but I was too excited to care. Finally a mouse that might be the one?</p>

<h2>Don't forget to do your own research...</h2>

<p>I ordered the mouse without checking if it was fully compatible with macOS. A risky move because without the right software you can't <strong>turn off the vomit inducing RGB lights</strong>.</p>

<p>Razer supported macOS with their peripherals for years so it must just work, right?</p>

<p>Turns out the required software (Razer Synapse V4) was not available for macOS when I unboxed the mouse. You needed to configure it on a Windows PC with Synapse V4 because older versions that work on macOS could not detect this mouse.</p>

<p>Naturally, I procrastinated doing that for almost a week and focused on work instead.</p>

<p>Thankfully, when I decided to pick up the mouse again and download the software to my PC, a button popped up on Razer's website saying that <strong>Razer Synapse V4 was available for Mac as an early preview.</strong></p>

<img src="https://cloud.wojtek.im/CQFlqm51VvGR6Jw2by13+" width="886" height="392" alt="Razer Synapse V4 now available on Mac" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>
  Another problem magically solved by procrastinating it away.
</p>

<h2>Configuration</h2>

<p>First thing that hits you in the face is you need to create a Razer account to configure the mouse, like you're signing up for Facebook... Completely unnecessary process that should be optional.</p>

<p>You have plenty of options to configure RGB, standby mode, and sensitivity. When it comes to remapping the actual buttons, that's where this thing falls short.</p>

<p>My biggest issue with the software was the inability to map <code>Back</code> or <code>Forward</code> globally to the side buttons. You could only use those in a "Web Browser" profile.</p>

<p>Strange choice to limit this feature because it is available in the software but only for a specific app profile.</p>

<p>Thankfully that's something that can be easily solved with third-party software like <strong>SteerMouse</strong>, which I love.</p>

<p>I set the top button as <code>⌘W</code> for closing tabs and windows while the bottom one was set to <code>Back</code>.</p>

<p>I won't even talk about the "AI" button gimmicks.</p>

<h2>Comfort</h2>

<p>This Razer mouse is simply not comfortable to use for me. The way it's profiled is very awkward. The arch of the hand lacks support and wants to slide off onto the desk when you try to grip in a relaxed position.</p>

<p>The mouse is too slippery to be picked up easily. The bumpy texture is nice to the touch and probably much more durable than on any other mouse but it doesn't give you a good grip.</p>

<img src="https://i.ytimg.com/vi/ofgvlbLNMoI/maxresdefault.jpg" width="648" height="364" alt="" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />
<p>
  via <a href="https://www.youtube.com/watch?v=ofgvlbLNMoI">Razer on YouTube</a>
</p>

<p>I guess they want you clawing this thing and lifting your wrist up from the desk like you're some kind of competitive gamer.</p>

<p>You either claw it and put tension in your wrist or have the tips of your fingers extend past the left/right click buttons and float in the air.</p>

<p><em>Kind of like when you put your toes past the edge of a shoe as a kid and they end up touching the pavement.</em></p>

<p>Using the mouse this way is not ergonomic or comfortable for longer periods of time. And if you have a relaxed grip, your wrist will end up sliding on the desk and rubbing on the bone too much.</p>

<p>Keep in mind this mouse is meant to be for productivity, not gaming. Their internal SKU is <code>RazerProClickV2-WirelessProductivityMouse-VerticalEdition</code>.</p>

<p>I really thought I could get used to it but after 3 weeks I ended up having <strong>way more pain in my wrist than when using a regular non-vertical mouse</strong> for extended periods of time.</p>

<p>The angle of the grip is just too extreme at 71.7° compared to MX Vertical's 57°.</p>

<p>---</p>

<p>Picking up a Logitech MX Vertical and using it for a few minutes really highlights how much better profiled it is compared to the Razer. You can feel the wrist strain and tension going away instantly. Logitech really nailed the ergonomics on this model.</p>

<h2>Conclusion</h2>

<blockquote>
While the product has impressive specs and technology, the ergonomics are simply not there for me.
</blockquote>

<p>I really wanted to like this mouse but I'm sad that it's so uncomfortable and clunky to use. I started the process of returning this mouse at the moment of writing this review. Had to pay $15 to ship it back to Razer in the Netherlands because they don't offer free returns in Europe.</p>

<p>Razer, stick to gaming mice and leave the productivity mice to people with more experience.</p>

<p>I guess we're back to waiting for Logitech to release an improved MX Vertical with a 1000Hz sensor.</p>

<img src="https://pbs.twimg.com/media/Eke-7kZXYAAci38?format=jpg&name=4096x4096" width="400" height="400" alt="" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />
<p>
  via <a href="https://x.com/Microsoft/status/1317231351754559492">Microsoft on Twitter</a>
</p>

<p>Lately, I've been using the <a href="https://www.rtings.com/mouse/reviews/logitech/g502-x-plus">Logitech G502 X Plus</a> with my Mac. It's got a Lightspeed 1000Hz sensor, free-spin scroll wheel, and a bunch of side buttons.</p>

<p>Tempted to go back to the MX Vertical again but the laggy cursor response is painful to watch.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a rich link preview React Server Component]]></title>
            <description><![CDATA[Make external links more clickable with better link previews.]]></description>
            <link>https://wojtek.im/journal/creating-a-link-embed-react-server-component</link>
            <guid isPermaLink="true">https://wojtek.im/journal/creating-a-link-embed-react-server-component</guid>
            <pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/creating-a-link-embed-react-server-component">https://wojtek.im/journal/creating-a-link-embed-react-server-component</a>.</em></p>

<p>The internet is full of links that lead to other places vying for your attention.</p>

<p>Wouldn't it be nice if we knew where those links took us before we clicked them?</p>

<p>Rich link previews give us <strong>instant context without leaving our current flow</strong>. They transform basic URLs into interactive elements that boost engagement by surfacing key metadata upfront.</p>

<li>Is that article worth reading?</li>
<li>Is that YouTube video clickbait?</li>
<li>Is that Tweet a banger?</li>

<p>You don't have to guess when a link gets presented with a small preview.</p>

<p>And while some platforms (like Instagram) might break their OG tag support, we can build better, more reliable experiences in our own apps. <a href="https://github.com/pugson/telegram-twitter-url-expand-bot">Or even in Telegram chats.</a></p>

<p>Let me show you how to implement robust link previews using the <a href="https://metadata.vision/?ref=wojtek.im">free metadata.vision API</a> and React Server Components (RSC).</p>

<p>You have a prime example right here. Which link would you rather click?</p>

<h2>Demo of what we’re building</h2>

<h2>Getting the data</h2>

<p>Let’s start by creating a server function that will fetch the OG metadata for a link.</p>

<pre><code>async function getMetadata({ site }: { site: string }) {
  try {
    const req = await fetch(<code>https://og.metadata.vision/${site}</code>, {
      next: { revalidate: 60 <em> 60 </em> 24 }, // Cache for 24 hours with Next.js
    });
    const response = await req.json();
    return response.data;
  } catch (error) {
    console.error(error);
    throw new Error(<code>Failed to fetch metadata for ${site}</code>);
  }
}
</code></pre>

<h2>Rendering the link preview</h2>

<p>Now we can create a React Server Component that will render the link preview.</p>

<p>1. By default, we will show a big video preview if the link has a video.<br />
2. If the link has no video, we will show a big image preview.<br />
3. If the link has no video or image, we will show a simple link with a favicon next to the title.</p>

<h3>Customizing</h3>

<p>Let’s give it some props that will let us style it differently depending on what kind of metadata we want to show.</p>

<li><code>noVideo</code> falls back to the image preview.</li>
<li><code>noImage</code> falls back to the simple link with a favicon.</li>
<li><code>compact</code> will make the image or video smaller and show it alongside the title and description.</li>

<blockquote>
  On smaller screens, the <code>compact</code> prop will not affect the layout as to not
  cramp the media and text too much.
</blockquote>

<h3>Final code</h3>

<p>Here is the code for our component, styled using Tailwind CSS:</p>

<p>``<code>link-preview.tsx</p>

<p>// Use clsx and tailwind-merge for handling conditional classnames<br />
const tw = (initial: any, ...args: any[]) => twMerge(clsx(initial, ...args));</p>

<p>type LinkPreviewProps = {<br />
  url: string;<br />
  noVideo?: boolean;<br />
  noImage?: boolean;<br />
  compact?: boolean;<br />
};</p>

<p>type MediaProps = {<br />
  src: string;<br />
  compact: boolean;<br />
};</p>

<p>const MediaWrapper = ({ children, compact }: { children: React.ReactNode; compact: boolean }) => (<br />
  <span<br />
    className={tw(<br />
      compact<br />
        ? "border-b border-border sm:relative sm:border-b-0 sm:border-r sm:border-border"<br />
        : "w-full border-b border-border"<br />
    )}<br />
  ><br />
    {children}<br />
  </span><br />
);</p>

<p>const PreviewImage = ({ src, compact }: MediaProps) => (<br />
  <Image<br />
    src={src}<br />
    alt=""<br />
    width={compact ? 256 : 1200}<br />
    height={compact ? 256 : 630}<br />
    className={tw("h-full w-full", compact && "sm:object-cover sm:object-center")}<br />
  /><br />
);</p>

<p>const PreviewVideo = ({ src, compact }: MediaProps) => (<br />
  <video<br />
    src={src}<br />
    width="100%"<br />
    height="auto"<br />
    muted<br />
    playsInline<br />
    loop<br />
    autoPlay<br />
    className={tw("h-full w-full", compact && "sm:object-cover sm:object-center")}<br />
  /><br />
);</p>

<p>const TitleAndDescription = ({<br />
  metadata,<br />
  compact,<br />
  domainOnly,<br />
  restOfTheUrl,<br />
  noImage<br />
}: {<br />
  metadata: any;<br />
  compact: boolean;<br />
  domainOnly: string;<br />
  restOfTheUrl: string;<br />
  noImage: boolean;<br />
}) => (<br />
  <span<br />
    className={tw(<br />
      "flex h-full flex-col justify-between p-4 pb-2.5",<br />
      compact && "min-w-0 sm:flex sm:h-full sm:flex-col sm:justify-center sm:px-4 sm:py-4 sm:pb-2.5"<br />
    )}<br />
  ><br />
    <span><br />
      <p><br />
        {(!metadata.image || noImage) && metadata.logo && (<br />
          <span className="block pb-2"><br />
            <Image<br />
              alt=""<br />
              src={metadata.logo}<br />
              width={28}<br />
              height={28}<br />
              className="-ml-0.5 inline-block rounded-md"<br />
            /><br />
          </p><br />
        )}<br />
        <span>{metadata.title}</span><br />
      </span><br />
      <p><br />
        {metadata.description}<br />
      </p><br />
    </span><br />
    <p><br />
      <span className="inline-block transition group-hover:text-blue-500"><br />
        {domainOnly}<br />
      </p><br />
      {restOfTheUrl !== "/" && (<br />
        <p><br />
          {restOfTheUrl}<br />
        </p><br />
      )}<br />
    </span><br />
  </span><br />
);</p>

<p>export async function LinkPreview({ url, noVideo, noImage, compact }: LinkPreviewProps) {<br />
  const metadata = await getMetadata({ site: url });</p>

<p>// Fallback to a regular link if there is no metadata<br />
  if (!metadata) {<br />
    return (<br />
      <a href={url} target="_blank"><br />
        {url}<br />
      </a><br />
    );<br />
  }</p>

<p>const { hostname: domainOnly, pathname: restOfTheUrl } = new URL(url);<br />
  const showImage = !noImage && metadata.image && (!metadata.video || noVideo);<br />
  const showVideo = !noVideo && !noImage && metadata.video;</p>

<p>return (<br />
    <Link<br />
      className={tw(<br />
        "group flex flex-col overflow-hidden rounded-2xl border border-border ring-blue-500 transition hover:border-blue-500 hover:ring-2 active:scale-[0.98]",<br />
        compact && "sm:grid sm:grid-cols-[10rem,1fr]"<br />
      )}<br />
      href={url}<br />
      target="_blank"<br />
      title={url}<br />
    ><br />
      {showImage && metadata.image && (<br />
        <MediaWrapper compact={!!compact}><br />
          <PreviewImage src={metadata.image} compact={!!compact} /><br />
        </MediaWrapper><br />
      )}<br />
      {showVideo && metadata.video && (<br />
        <MediaWrapper compact={!!compact}><br />
          <PreviewVideo src={metadata.video} compact={!!compact} /><br />
        </MediaWrapper><br />
      )}<br />
      <TitleAndDescription<br />
        metadata={metadata}<br />
        compact={!!compact}<br />
        domainOnly={domainOnly}<br />
        restOfTheUrl={restOfTheUrl}<br />
        noImage={!!noImage}<br />
      /><br />
    </Link><br />
  );<br />
}</p>

<p></code>``</p>

<p>This should give you a good base to work with. Tweak the styles to your liking or remove Tailwind altogether.</p>

<p>If you found this useful, <a href="https://x.com/pugson">follow me on Twitter</a> or subscribe to the newsletter.</p>

<hr />
<p>
  Thanks to <a href="https://x.com/saintjcob">Jakub Ziemba</a> and <a href="https://brandon.mn/?ref=wojtek.im">Brandon
  Johnson</a> for feedback on an earlier draft
  of this post.
</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Displaying the weekly downloads count of your NPM package on your Next.js website]]></title>
            <description><![CDATA[Show a live count on your website with a simple function.]]></description>
            <link>https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs</link>
            <guid isPermaLink="true">https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs</guid>
            <pubDate>Sun, 02 Jun 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs">https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs</a>.</em></p>

<p>I have a grid of project tiles on my <a href="https://wojtek.im/projects">homepage</a> that show dynamic stats at the bottom. It completes the design and makes these tiles a bit more interesting.</p>

<img src="https://wojtek.im/journal-assets/project-tiles.png" width="640" height="306" alt="" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>Being able to see the stats change over time is really motivating. Small, but needed push to keep working on these projects.</p>

<h2>Looking for a way in...</h2>

<p>NPM does not have any documentation about their private API that I could find. Snooping around network requests does not reveal any useful endpoints for grabbing this data because they are rendering each package page on the server.</p>

<p>After digging around the internet for a few minutes I found an <a href="https://www.edoardoscibona.com/exploring-the-npm-registry-api#example-5-count-downloads-for-a-package">article by Edoardo Scibona</a> that explains how to use NPM's registry API endpoint to get the weekly downloads count for a package.</p>

<h2>Getting the data</h2>

<p>You can get the downloads count of a package published to NPM with a simple fetch to this endpoint:</p>

<p><code>https://api.npmjs.org/downloads/point/<period>/<package></code></p>

<p>Replace <code><package></code> with your package name and <code><period></code> with one of these supported values:</p>

<li><code>last-day</code></li>
<li><code>last-week</code></li>
<li><code>last-month</code></li>
<li><code>last-year</code></li>

<h2>Packaging it up into a function for Next.js</h2>

<p>Now we can create a simple function that will fetch the data for us and cache it for an hour.</p>

<p>That's where Next.js' <code>revalidate</code> comes into play.</p>

<p>You can lower the value if you want to have more up-to-date stats, but keep in mind that it will cause more incremental static regenerations <a href="https://vercel.com/docs/incremental-static-regeneration">(ISR)</a> on Vercel when people visit your site.</p>

<p>``<code>npm-stats.tsx<br />
type Period = "last-day" | "last-week" | "last-month" | "last-year";</p>

<p>async function getNPMPackageDownloads(packageName: string, period: Period) {<br />
  const res = await fetch(<br />
    </code>https://api.npmjs.org/downloads/point/${period}/${packageName}<code>,<br />
    {<br />
      next: { revalidate: 3600 }, // 60 (s) * 60 (min) = 3600 seconds (1 hour)<br />
    },<br />
  );<br />
  return res.json();<br />
}</p><pre><code><blockquote>
  ⚠️ Warning: If you omit the </code>revalidate<code> option, the data will remain cached
  and your stats will be stale even across deploys.
</blockquote>

<h2>Displaying the data</h2>

Using the function inside a React Server Component is as simple as importing a hook. Here is an example:

</code></pre><p>page.tsx<br />
export async function Projects() {<br />
  const { downloads } = await getNPMPackageDownloads(<br />
    "react-farcaster-embed",<br />
    "last-week",<br />
  );</p>

<p>return (<br />
    <p><br />
      react-farcaster-embed has ${downloads.toLocaleString()} weekly<br />
      downloads<br />
    </p><br />
  );<br />
}<br />
</code>`<code></p>

<p>If you want to do the same thing for GitHub stars, you can use the GitHub API and extend your function with another fetch. Just remember to use </code>Promise.all()` for best performance.</p>

]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Getting Payload CMS deployed on Railway]]></title>
            <description><![CDATA[Application failed to respond? You need to set the PORT environment variable.]]></description>
            <link>https://wojtek.im/journal/deploying-payload-cms-to-railway</link>
            <guid isPermaLink="true">https://wojtek.im/journal/deploying-payload-cms-to-railway</guid>
            <pubDate>Sun, 03 Mar 2024 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/deploying-payload-cms-to-railway">https://wojtek.im/journal/deploying-payload-cms-to-railway</a>.</em></p>

<p>Making a new project with <a href="https://payloadcms.com/">Payload CMS</a>?</p>

<p>Trying to deploy it on <a href="https://railway.app?referralCode=dev">Railway</a> but all you're getting is a vague <code>Application failed to respond</code> screen, no errors in the console, and completely no reference in the docs?</p>

<p>The fix is dead simple. You need to set the <code>PORT</code> environment variable to <code>3000</code> in your Railway environment.</p>

<img src="https://wojtek.im/journal-assets/payload-cms-railway.png" width="700" height="220" alt="Railway environment variables dashboard" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>Good luck with the rest ✌️</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Open Source: react-farcaster-embed]]></title>
            <description><![CDATA[Display an embedded cast from Farcaster in your React app.]]></description>
            <link>https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app</link>
            <guid isPermaLink="true">https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app</guid>
            <pubDate>Thu, 28 Dec 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app">https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app</a>.</em></p>

<p><div className="pt-8" /><br />
<FarcasterEmbed url="https://farcaster.xyz/pugson/0x4294c797" /></p>

<p>Inspired by Vercel's <a href="https://react-tweet.vercel.app/">react-tweet</a>, I built a React component to embed Farcaster casts in your React app or blog.</p>

<p>You can embed it by passing in the URL of the cast, or the username and hash. Everything gets pulled in automatically.</p>

<pre><code><FarcasterEmbed url="https://farcaster.xyz/pugson/0x4294c797" />
<FarcasterEmbed username="dwr" hash="0x48d47343" />
</code></pre>

<h2>Get Started</h2>



<h2>Features</h2>

<li>Supports server components and client components</li>
<li>Shows the cast's author, their avatar and username, date when the cast was posted</li>
<li>Renders the cast's content with links</li>
<li>Shows the channel name and avatar</li>
<li>Shows counts for replies, likes, recasts + quotes, watches</li>
<li>Adds a link to the cast on Farcaster</li>
<li>Renders images attached to the cast</li>
<li>Embeds a video player for videos attached to the cast</li>
<li>Shows quoted casts</li>
<li>Rich Open Graph previews for links in the cast</li>

<h2>Examples</h2>

<p><FarcasterEmbed url="https://farcaster.xyz/pugson/0x1c80519c95bc2dcdf772b9573d34b11374b6a3c9" /><br />
<FarcasterEmbed url="https://farcaster.xyz/ted/0x04120d52" /><br />
<FarcasterEmbed url="https://farcaster.xyz/samantha/0xaa9d7ff2" /><br />
<FarcasterEmbed url="https://farcaster.xyz/pugson/0xfc256382" /><br />
<FarcasterEmbed url="https://farcaster.xyz/zachterrell/0xbf3457ec" /><br />
<FarcasterEmbed url="https://farcaster.xyz/dwr.eth/0x58b1409e" /><br />
<FarcasterEmbed url="https://farcaster.xyz/greg/0xd4c5a131" /><br />
<FarcasterEmbed url="https://farcaster.xyz/nonlinear.eth/0xbf8a6f9a" /><br />
<FarcasterEmbed url="https://farcaster.xyz/pugson/0xcf0ef904" /></p>

<h2>Usage</h2>

<p>Head over to <a href="https://github.com/pugson/react-farcaster-embed">GitHub</a>.</p>

]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting up Simple Analytics for Next.js (App Router)]]></title>
            <description><![CDATA[Learn how to configure external analytics with a noscript fallback for Next.js 13 and 14 using the App Router.]]></description>
            <link>https://wojtek.im/journal/setting-up-simple-analytics-for-next-13</link>
            <guid isPermaLink="true">https://wojtek.im/journal/setting-up-simple-analytics-for-next-13</guid>
            <pubDate>Thu, 16 Nov 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><em>For the full reading experience, visit <a href="https://wojtek.im/journal/setting-up-simple-analytics-for-next-13">https://wojtek.im/journal/setting-up-simple-analytics-for-next-13</a>.</em></p>

<hr />
<p>
  Disclaimer: Links to <a href="https://www.simpleanalytics.com/?referral=wojtek">Simple
  Analytics</a> on this page are
  affiliate links. If you sign up for a paid plan using my referral code, I will
  get a small commission.
</p>
<hr />

<p>If you’re worried Vercel’s built-in analytics product might at some point randomly charge you $300 overnight (😰), you probably have another analytics service that you use for sites and apps.</p>

<p>In my case that’s <strong>Simple Analytics</strong> which I’ve been happily using for over 6 years with no surprises.</p>

<h2>Using external analytics with the App Router</h2>

<p>Things have gotten slightly complicated with Next.js 13 and 14 when using the App Router.</p>

<li>Vercel’s docs about <a href="https://nextjs.org/docs/app/building-your-application/optimizing/scripts">Script Optimization</a> are not very clear on how to set up external analytics that have a <code><noscript></code> fallback.</li>

<li><code>next/head</code> <strong>is being deprecated</strong> and is not recommended for React Server Components and SSR.</li>

<li>You can still use <code>next/head</code> with the App Router rather than the <code>metadata</code> object but the penalties are unclear.</li>

<p>Here is a snippet that uses the new <code>next/script</code> method while also providing a <code><noscript></code> fallback when someone has JavaScript disabled in their browser.</p>

<p>Add it to your <code>app/layout.tsx</code> file:</p>

<pre><code>export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <noscript>
          <img
            src="https://<your_custom_sa_domain>/noscript.gif?collect-dnt=true&hostname=<your_website_domain>"
            alt=""
            referrerPolicy="no-referrer-when-downgrade"
          />
        </noscript>
      </body>
      <Script
        strategy="afterInteractive"
        async
        defer
        src="https://<your_custom_sa_domain>/latest.js"
        data-collect-dnt="true"
        data-hostname="<your_website_domain>"
      />
    </html>
  );
}
</code></pre>

<p>
✦ You can adapt the code above to work with any other analytics provider that also has a <code><noscript></code> fallback option.
</p>

<h2>Getting all the data</h2>

<p>To make sure all your visits are being tracked correctly, you should set up a <strong>custom subdomain</strong> for <a href="https://www.simpleanalytics.com/?referral=wojtek">Simple Analytics</a>. This way your script will <a href="https://docs.simpleanalytics.com/bypass-ad-blockers">bypass ad blockers</a>.</p>

<p>
  ✦ Avoid using common words like _analytics_ or _stats_ in your subdomain name.
</p>

<p>I’m also using the <code>data-collect-dnt</code> attribute to overwrite the default behavior of not tracking people with DNT enabled.</p>

<p>There is <strong>no identifiable information</strong> being collected so I don’t see a reason not to track those visits. <strong>Everything is anonymous.</strong></p>

<img src="https://wojtek.im/journal-assets/sa-screenshot.png" width="700" height="220" alt="Dashboard showing the most recent visitors to my website." style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>If you have any questions or this guide is completely wrong, feel free to reach out on <a href="https://twitter.com/pugson">Twitter</a> or <a href="https://t.me/pugson">Telegram</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[New year, new me?]]></title>
            <description><![CDATA[Refreshing my personal website for 2023. Plus some words about burnout, FOMO, and finding motivation.]]></description>
            <link>https://wojtek.im/journal/new-year-new-me-2023</link>
            <guid isPermaLink="true">https://wojtek.im/journal/new-year-new-me-2023</guid>
            <pubDate>Wed, 26 Jul 2023 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<img src="https://wojtek.im/journal-assets/next13-site-git-history.png" width="700" height="220" alt="Starting the repo for this website 4 months ago." style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>It’s not 2024 yet but it might as well be...</p>

<p>Time has finally come to take a stab at refreshing my personal website — and actually shipping this time.</p>

<p>After <strong>4 months of experiments, traveling, and (years of) procrastination</strong>, I’m happy to finally get this thing published. 🥳</p>

<p>The <a href="https://v1.wojtek.im" target="_blank">previous iteration</a> has been online for over 3 years now, and while it dazzled passersby folks with its flashy animated WebGL blob and vibrant colors, it did not serve as a good place to showcase work or publish my thoughts.</p>

<p>Going for a modular and toned down approach this time. This site needs to survive the next few years in a way that will enable expressing myself online without getting in the way.</p>

<hr />

<p>
  <strong>Next journal entry will be about setting up a simple Next.js 13 + MDX blog
  like this for yourself. Drop your email below if you don’t want to miss it.</strong>
</p>

<p><NewsletterCapture embedded /></p>

<hr />

<h2>"Well, how did I get here?"</h2>

<p>I built the old site using <a href="https://github.com/pugson/parcel-static-template">an old Parcel starter kit 💀</a> which ended up breaking completely with a Node update, making it impossible to build locally. So there I was, stuck with a website that could only be updated by pushing changes to GitHub and waiting for Netlify to build it. Every developer’s dream.</p>

<blockquote>
  Only way to keep the modern web alive is to never touch it again or spend
  weeks rewriting everything from scratch.
</blockquote>

<h2>What’s on the roadmap?</h2>

<p>I mentioned a modular and iterative approach earlier. Here are some of the things I want to add in the near future:</p>

<li>showcase of my user interface work</li>
<li><a href="/friends">friends page where I can recommend talented folks</a></li>
<li>segmented newsletter options where you can subscribe to only the topics you care about</li>
<li>much better styling and formatting for journal entries</li>
<li>cleaning up design inconsistencies</li>
<li>bookmarks for highlighting cool stuff from the internet</li>
<li>making a seamless loop of the ✌️ emoji 3D render on the homepage (it stutters a bit)</li>
<li>light mode for reading</li>
<li>a moodboard for my current obsessions (tumblr vibes)</li>

<hr />

<h2>You’re only as good as 1,000 of your worst creations</h2>

<p>After hitting many roadblocks by experiencing <strong>burnout</strong> at the end of 2021, <strong>imposter syndrome</strong> telling me that I will never design anything good again, and a whole lot of <strong>executive dysfunction</strong>, I am glad to be back with new ideas, new skills, and more energy this year.</p>

<p>Spending too much time on Twitter looking at other people’s work and comparing myself to them was a huge source of anxiety and stress. The <strong>constant FOMO of everyone shipping new projects</strong> was eating me alive. It felt like 24 hours in a day were not even close to being enough to keep up with the pace of the industry.</p>

<p>I knew I could get to that level if I put in the hours but every attempt was a failure. Every failure made me more angry and unhappy because I wanted to create something great on the first try — something that never happens. Sound familiar?</p>

<img src="https://wojtek.im/journal-assets/vin-tweet.png" width="700" height="600" alt="A quote by internetVin etched out in wood, saying: ’When starting something new, your focus has to be on producing a large quantity of output. If you instead aim for quality your skill level won’t be able to achieve your vision of quality and you will give up. Making terrible things is part of the process.’" style="display: block; margin: 0 auto; height: auto; max-width: 100%;" />

<p>Taking a 6+ month break finally taught me to stop chasing perfection and start taking small steps towards reachable goals.</p>

<p>
  ✦ This is how <a href="https://ensdata.net">ENS Data</a> and the <a href="https://github.com/pugson/telegram-twitter-url-expand-bot">Link Expander Telegram
  Bot</a> were made.
</p>

<p>Seeing some of my closest friends struggle with similar problems made me want to help them, so I created a Telegram group chat where we shared progress updates and talked about all things "product".</p>

<p>In effect, the group ended up reciprocating that energy — optimism and motivation started naturally flowing in my direction. <strong>Having a group of likeminded friends to talk to and bounce around ideas with is something I would recommend to everyone.</strong></p>

<h2>Wait, the struggle is... real?</h2>

<p>During this process I also learned that my brain’s dopamine receptors are actually broken and I’ve had severe ADHD for the past decade. This explains a lot of struggles with productivity, motivation, broken promises, and some of my behavior in various parts of life.</p>

<p>After talking with a couple friends and the YouTube algorithm suggesting ADHD videos everything started to click... but more on that in a future journal. Working on getting a clinical diagnosis and figuring out how to manage it this year.</p>

<hr />

<p>It might also explain why this whole journal is disorganized with thoughts skipping all over the place. Writing and publishing things longer than 140 characters on my own domain is at least a small step in the right direction.</p>

<h2>Flipping the switch</h2>

<p>Can’t precisely count how many hours this site took to design and build but I estimate it to be around 60-80 <a href="https://youtu.be/c0-hvjV2A5Y?si=fEEk9rLKQx3SidAc">Fred Again Boiler Rooms</a>.</p>

<p>Before, I would obsess over every tiny detail and never launch it but this time it’s <strong>good enough to ship.</strong></p>

<p>Saying goodbye to the <a href="https://v1.wojtek.im" target="_blank">old site</a> and hello to the new one.</p>

<p>See you around?</p>

<p><video<br />
  src="https://cloud.wojtek.im/lfK206mSqmTwYYdlQV9b+"<br />
  loop<br />
  autoPlay<br />
  muted<br />
  playsInline<br />
/></p>

<p>
  ✦ Yes, there is a weird jump when navigating from the journal and the video
  looks 30 FPS. And no, I am not gonna re-record it. Learning to embrace
  imperfections.
</p>

<h2>Inspiration</h2>

<p>The structure and design of this site has been inspired by many great people and their projects, including:</p>

<li><a href="https://www.internetvin.tv/?ref=wojtek.im">internetVin</a></li>
<li><a href="https://www.mendicantbias.xyz/?ref=wojtek.im">Mendicant Bias</a></li>
<li><a href="https://lochieaxon.com/?ref=wojtek.im">Lochie Axon</a></li>
<li><a href="https://udara.io/?ref=wojtek.im">Udara Jay</a></li>
<li><a href="https://paco.me/?ref=wojtek.im">Paco Coursey</a></li>
<li><a href="https://rauno.me/?ref=wojtek.im">Rauno Freiberg</a></li>
<li><a href="https://emilkowal.ski/?ref=wojtek.im">Emil Kowalski</a></li>
<li><a href="https://shen.land/?ref=wojtek.im">Shen</a></li>
<li><a href="https://twitter.com/floguo">Flo Guo</a></li>
<li><a href="https://www.jesswang.art/?ref=wojtek.im">Jess Wang</a></li>
<li><a href="https://pifafu.com/?ref=wojtek.im">Kathy Zheng</a></li>
<li><a href="https://lucas.love/?ref=wojtek.im">Lucas Fischer</a></li>
<li><a href="https://steventey.com/?ref=wojtek.im">Steven Tey</a></li>
<li><a href="https://nelson.co/?ref=wojtek.im">Gavin Nelson</a></li>
<li><a href="https://www.joshwcomeau.com/?ref=wojtek.im">Josh W. Comeau</a></li>
<li><a href="https://bob.obringer.net/?ref=wojtek.im">Bob Obringer</a></li>
<li><a href="https://www.nan.fyi/?ref=wojtek.im">Nanda Syahrasyad</a></li>
<li><a href="https://bijani.com/?ref=wojtek.im">Jacob Bijani</a></li>
<li><a href="https://tigris.li/?ref=wojtek.im">Tigris Li</a></li>
<li><a href="https://sania.io/?ref=wojtek.im">Sania Saleh</a></li>
<li><a href="https://ibuildmyideas.com/?ref=wojtek.im">Jordan Singer</a></li>
<li><a href="https://www.avalleskey.com/?ref=wojtek.im">Austin Valleskey</a></li>
<li><a href="https://jasonyuan.design/?ref=wojtek.im">Jason Yuan</a></li>
<li><a href="https://www.sulley.me/?ref=wojtek.im">Suleiman Zakari Mohammed</a></li>
<li><a href="https://twitter.com/jonnotie">Jonno Riekwel</a></li>
<li><a href="https://webb.page/?ref=wojtek.im">Paul Anthony Webb</a></li>
<li><a href="https://matthewmorek.com/?ref=wojtek.im">Matthew Morek</a></li>
<li><a href="https://twitter.com/gregskril">Greg Skriloff</a></li>
<li><a href="https://ped.ro/?ref=wojtek.im">Pedro Duarte</a></li>
<li><a href="https://twitter.com/duyluongdesign">Duy Luong</a></li>
<li><a href="https://krausefx.com/?ref=wojtek.im">Felix Krause</a></li>
<li><a href="https://www.wireframe.co/?ref=wojtek.im">James McDonald</a></li>
<li><a href="https://hector.me/?ref=wojtek.im">Hector Simpson</a></li>

<p>Thanks to everyone for sharing their work throughout all these years and pushing the web forward. Seeing so many talented people create amazing things makes me excited for the future.</p>]]></content:encoded>
        </item>
    </channel>
</rss>