<![CDATA[wojtek.im]]> https://wojtek.im/journal https://wojtek.im/face.jpg wojtek.im https://wojtek.im/journal wojtek.im Fri, 20 Mar 2026 10:31:36 GMT <![CDATA[You need PgBouncer when deploying Postgres on Railway]]> https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients Tue, 03 Mar 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/fix-postgres-on-railway-pgbouncer-too-many-clients.

2026-03-02 22:12:19.982 UTC [56699] FATAL: sorry, too many clients already
2026-03-02 22:12:20.459 UTC [56700] FATAL: sorry, too many clients already
2026-03-02 22:12:22.046 UTC [56701] FATAL: sorry, too many clients already
2026-03-02 22:12:28.046 UTC [56702] FATAL: sorry, too many clients already

Looks familiar?

This can happen to your app and your users can lose work because of maxed out connections to Postgres.

Railway does not let you manually edit the max_connections config anywhere in their dashboard. Probably for the better, since this would be a bandaid and not a proper fix.

This is where PgBouncer comes in.

CleanShot 2026-03-03 at 12.25.11.AM@2x.png

Install the template made by Brody and it will automatically pre-fill all the necessary Postgres variables from your Railway project.

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.

After you are done deploying the template, go ahead and create a public domain for PgBouncer in the settings. After that is done update your app's environment variables to use the new DATABASE_PUBLIC_URL from the PgBouncer service which you can easily copy from the Variables screen.

Important: 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 DATABASE_URL_DIRECT and point your ORM config directly to the database.

Optimizing for serverless

For an app that uses Next.js, transaction 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.

You can set PGBOUNCER_POOL_MODE to transaction to enable this.

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.

Drizzle, which runs on node-postgres under the hood, does not use prepared statements so you are good to go out of the box.

More reading about different pooling modes here:

]]>
<![CDATA[What’s stopping you now?]]> https://wojtek.im/journal/whats-stopping-you-now https://wojtek.im/journal/whats-stopping-you-now Wed, 11 Feb 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/whats-stopping-you-now.

Remember that to-do list from 2 years ago? Those tasks that seemed impossible to do. If only “you had more time” to focus.

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.

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.

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.

By the time I got a reply I had a fully fleshed out custom CMS integrated into my website with even more features.

Of course the answer from the founder was "no".

I'm writing this post using the rich editor Claude built.

CleanShot 2026-02-11 at 03.16.24.PM@2x.png

If you can’t spare 8 hours each day to get a big project done, do something small and see the compounding effects later.

Get unstuck. Gain momentum. Even if it's not good enough, it's much easier to iterate on something that exists than starting off with a blank slate.

]]>
<![CDATA[You should build it anyway]]> https://wojtek.im/journal/you-should-build-it-anyway https://wojtek.im/journal/you-should-build-it-anyway Wed, 14 Jan 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/you-should-build-it-anyway.

The mantra of "Why would you make this when [thing] exists already?" is played out.

With the rise of AI tools like Claude Code and Cursor there is no excuse not to build something you want, exactly how you want it.

People love to laugh and complain about yet another to-do app but they all have a reason for existing.

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.
  • High subscription price? Build your own.

  • Don't like the design? Build your own.

  • Lack of specific features? Build your own.

  • Scared it might shut down in a year? Build your own.

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.

Those who end up cooking something that tastes great to the masses are able to turn it into a business.

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.

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.

Software for one+

Build it for yourself first then share it with friends.

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.

(This is what happened with my iOS app Pixeldrop)

But if they don't, they can create their own version because there's nothing stopping them now.

Use existing systems

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.

I use an Oura Ring for sleep tracking but I didn't like how my sleep pattern data was displayed in the Oura app.

Looking around the web for solutions produced results that were either too much effort to set up or did not match my mental model of what data I wanted to focus on.

So instead I found the Oura API and looked at what I can extract from the responses. This kicked off a custom interactive dashboard with my sleep data.

CleanShot 2026-01-14 at 07.23.55.PM@2x.png

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.

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.


If you need an affordable and performant way to host your app, bot, or microservice I can recommend using Railway.

If you haven't tried Claude Code yet, you can use my guest pass and get a free week by using this link.

]]>
<![CDATA[Autoplay does not work on Mobile Safari in Low Power Mode]]> https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode Mon, 12 Jan 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/safari-autoplay-not-working-in-low-power-mode.

You probably ran into a bug where your video that had muted, loop, playsinline, autoplay properties set was not playing and had a giant play button overlay on it when using Safari.

This happens due to Low Power Mode. Apple prevents automatic playback of any video with LPM enabled.

Unfortunately there is no way around it other than getting the user to press the native play button or a custom one that calls the play() function on the video element.

No way to detect it

iOS does not expose a way to detect if Low Power Mode is enabled to prevent fingerprinting risk when websites track you.

Three.js and React Three Fiber also affected

With Low Power Mode enabled, you will not be able to autoplay a video plane inside a Three.js scene either without user interaction.

WebP workaround for short videos

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.

Only supported in Safari 16 or newer. Safari 14 and 15 have partial support but you might encounter issues.

You're going to need to use ffmpeg to convert your video into WebP first.

ffmpeg -i VIDEO_FILE.mp4 -vcodec libwebp -lossless 0 -q:v 80 -loop 0 -an OUTPUT.webp

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 <img> with a WebP source in its place.

However, I truly do not recommend this method as the WebP file will most likely be much larger than the original video and take longer to load + use even more resources.

Easier to just let go and give up. You win this one, Tim Apple. 😩

]]>
<![CDATA[Favorite Personal Sites of 2025]]> https://wojtek.im/journal/favorite-personal-sites-of-2025 https://wojtek.im/journal/favorite-personal-sites-of-2025 Fri, 09 Jan 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/favorite-personal-sites-of-2025.

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.

I'm just going to drop the links without saying what I like about each site. You can form your own unbiased opinion.

Hope these inspire you. Some might surprise you.

Presented in no particular order.

If you were on the fence about creating your own personal site, just do it. It has compounding effects.

But please make it your own and not just a blatant copy of someone else's website. No stealing and pretending you had an original idea without giving credit, okay?


Want to include rich link previews like these on your own website?

]]>
<![CDATA[How to target Safari with a CSS @supports media query]]> https://wojtek.im/journal/targeting-safari-with-css-media-query https://wojtek.im/journal/targeting-safari-with-css-media-query Sat, 03 Jan 2026 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/targeting-safari-with-css-media-query.

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

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.

Looking through the compatibility table on caniuse.com you can spot that Safari 16 and above has a unique property which is not supported by Chrome or Firefox called hanging-punctuation.

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

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

✦ 2024 Update: You can also chain it with font: -apple-system-body to make this more bulletproof. Thanks to Saber Hayati for the tip.

Demo

If you open this page in Safari, the right column will be red.


All Browsers


Safari only

Using this method with Tailwind CSS v3

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

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

Using this method with Tailwind CSS v4

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

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

The @slot marker indicates where the utility styles get inserted. Usage is the same as in v3:

Safari only

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

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.

That’s the case until these apps ship with their own rendering engines which will start being possible with iOS 17.4 for the EU.

]]>
<![CDATA[Keychron M5 Vertical Mouse as a Mac user]]> https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac Sun, 05 Oct 2025 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/keychron-m5-vertical-mouse-review-mac.

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

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 MX Master 4 that came out this month.

Razer Pro Click V2 Vertical was awful and I returned it.

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 $70 in the US and around $95 in Europe.

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

Keychron M5 Vertical

Polling Rate

This mouse can go up to 8000Hz polling rate 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.

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

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

Battery Life

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.

Software

Keychron has their own app for configuring the mouse and it's a web app! Which means no bloated software to install, no updates to worry about, and you can configure it from any device.

You can remap buttons to key combos like ⌘W natively, which is great.

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

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.

That can be easily solved with SteerMouse but I decided to keep it simple and use default settings while I'm still getting used to this mouse.

Keychron M5 Vertical bottom view

Photo courtesy of LTT Labs

Comfort

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.

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

Here are a few other gripes:

  • Not enough weight. Feels a bit too light. I would prefer just a tiny bit more substance to get rid of that cheap plastic feel.
  • Not enough grip. 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?
  • Horizontal scroll wheel is too close to the side buttons. 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.
  • Keychron M5 Vertical angle Keychron M5 Vertical angle

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

    Conclusion

    Keychron M5 Vertical packaging

    Photo courtesy of LTT Labs

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

    However, it is not perfect.

    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.

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

    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.

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

    Why is this backwards?

    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.


    For full technical specs and a more detailed review, check out LTT Labs.

    ]]>
    <![CDATA[Best React Native UI resources for creating beautiful apps]]> https://wojtek.im/journal/best-react-native-ui-resources https://wojtek.im/journal/best-react-native-ui-resources Tue, 23 Sep 2025 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/best-react-native-ui-resources.

    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 misconception. Just look how many popular apps have been built with Expo and React Native.

    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 Family.

    It all comes down to craft.

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

    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.

    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.

    I've compiled a list of things that I keep coming back to.


    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.


    Animate React Native

    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.

    Give the demo app a spin and see for yourself.

    Make It Animated

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

    You get full access to the source code and even a mobile app to try out the animations on your own devices.

    React Native Components

    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 Landing Components.

    Reactiive

    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
    with Skia, Reanimated and Gesture Handler.

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

    React Native Glow

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

    Zeego

    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.

    Plus, on iOS you can always reach into react-native-ios-context-menu directly if you need.

    Galeria

    Beautiful image gallery library for adding photo grids and zoomable images with natural gestures.

    react-native-fast-squircle

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

    callstack/liquid-glass

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

    Spotted in Prod

    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.

    Tiny humblebrag but my app, Pixeldrop, is also featured on SIP 😇

    ---

    Hope some of these resources end up being useful.

    More to come.

    Subscribe to the newsletter below to get notified when I publish updates to this list. Or follow the Telegram channel.

    ]]>
    <![CDATA[An expensive disappointment: Razer Pro Click V2 Vertical]]> https://wojtek.im/journal/razer-pro-click-v2-vertical-review https://wojtek.im/journal/razer-pro-click-v2-vertical-review Mon, 26 May 2025 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/razer-pro-click-v2-vertical-review.

    I've been in search of a perfect workspace mouse for a long time.

    Even considered trying to build my own out of parts from a Logitech gaming mouse with a Lightspeed sensor to fit into the MX Vertical enclosure.

    {/ 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. /}

    Some people on Reddit have attempted to do this but the results are not ideal.

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

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

    Meet the Razer Pro Click V2 Vertical

    Razer Pro Click V2 Vertical from two angles

    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. 🤢

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

    It was time to hit that Buy Now button on Razer's website and try it out for myself.

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

    Don't forget to do your own research...

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

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

    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.

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

    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 Razer Synapse V4 was available for Mac as an early preview.

    Razer Synapse V4 now available on Mac

    Another problem magically solved by procrastinating it away.

    Configuration

    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.

    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.

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

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

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

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

    I won't even talk about the "AI" button gimmicks.

    Comfort

    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.

    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.

    via Razer on YouTube

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

    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.

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

    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.

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

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

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

    ---

    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.

    Conclusion

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

    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.

    Razer, stick to gaming mice and leave the productivity mice to people with more experience.

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

    via Microsoft on Twitter

    Lately, I've been using the Logitech G502 X Plus with my Mac. It's got a Lightspeed 1000Hz sensor, free-spin scroll wheel, and a bunch of side buttons.

    Tempted to go back to the MX Vertical again but the laggy cursor response is painful to watch.

    ]]>
    <![CDATA[Building a rich link preview React Server Component]]> https://wojtek.im/journal/creating-a-link-embed-react-server-component https://wojtek.im/journal/creating-a-link-embed-react-server-component Thu, 09 Jan 2025 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/creating-a-link-embed-react-server-component.

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

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

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

  • Is that article worth reading?
  • Is that YouTube video clickbait?
  • Is that Tweet a banger?
  • You don't have to guess when a link gets presented with a small preview.

    And while some platforms (like Instagram) might break their OG tag support, we can build better, more reliable experiences in our own apps. Or even in Telegram chats.

    Let me show you how to implement robust link previews using the free metadata.vision API and React Server Components (RSC).

    You have a prime example right here. Which link would you rather click?

    Demo of what we’re building

    Getting the data

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

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

    Rendering the link preview

    Now we can create a React Server Component that will render the link preview.

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

    Customizing

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

  • noVideo falls back to the image preview.
  • noImage falls back to the simple link with a favicon.
  • compact will make the image or video smaller and show it alongside the title and description.
  • On smaller screens, the compact prop will not affect the layout as to not cramp the media and text too much.

    Final code

    Here is the code for our component, styled using Tailwind CSS:

    ``link-preview.tsx

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

    type LinkPreviewProps = {
    url: string;
    noVideo?: boolean;
    noImage?: boolean;
    compact?: boolean;
    };

    type MediaProps = {
    src: string;
    compact: boolean;
    };

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

    );

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

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

    const TitleAndDescription = ({
    metadata,
    compact,
    domainOnly,
    restOfTheUrl,
    noImage
    }: {
    metadata: any;
    compact: boolean;
    domainOnly: string;
    restOfTheUrl: string;
    noImage: boolean;
    }) => (
    className={tw(
    "flex h-full flex-col justify-between p-4 pb-2.5",
    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"
    )}
    >


    {(!metadata.image || noImage) && metadata.logo && (

    alt=""
    src={metadata.logo}
    width={28}
    height={28}
    className="-ml-0.5 inline-block rounded-md"
    />


    )}
    {metadata.title}


    {metadata.description}





    {domainOnly}


    {restOfTheUrl !== "/" && (


    {restOfTheUrl}


    )}


    );

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

    // Fallback to a regular link if there is no metadata
    if (!metadata) {
    return (

    {url}

    );
    }

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

    return (
    className={tw(
    "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]",
    compact && "sm:grid sm:grid-cols-[10rem,1fr]"
    )}
    href={url}
    target="_blank"
    title={url}
    >
    {showImage && metadata.image && (



    )}
    {showVideo && metadata.video && (



    )}
    metadata={metadata}
    compact={!!compact}
    domainOnly={domainOnly}
    restOfTheUrl={restOfTheUrl}
    noImage={!!noImage}
    />

    );
    }

    ``

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

    If you found this useful, follow me on Twitter or subscribe to the newsletter.


    Thanks to Jakub Ziemba and Brandon Johnson for feedback on an earlier draft of this post.

    ]]>
    <![CDATA[Displaying the weekly downloads count of your NPM package on your Next.js website]]> https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs Sun, 02 Jun 2024 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/displaying-weekly-downloads-count-of-npm-package-nextjs.

    I have a grid of project tiles on my homepage that show dynamic stats at the bottom. It completes the design and makes these tiles a bit more interesting.

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

    Looking for a way in...

    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.

    After digging around the internet for a few minutes I found an article by Edoardo Scibona that explains how to use NPM's registry API endpoint to get the weekly downloads count for a package.

    Getting the data

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

    https://api.npmjs.org/downloads/point//

    Replace with your package name and with one of these supported values:

  • last-day
  • last-week
  • last-month
  • last-year
  • Packaging it up into a function for Next.js

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

    That's where Next.js' revalidate comes into play.

    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 (ISR) on Vercel when people visit your site.

    ``npm-stats.tsx
    type Period = "last-day" | "last-week" | "last-month" | "last-year";

    async function getNPMPackageDownloads(packageName: string, period: Period) {
    const res = await fetch(
    https://api.npmjs.org/downloads/point/${period}/${packageName},
    {
    next: { revalidate: 3600 }, // 60 (s) * 60 (min) = 3600 seconds (1 hour)
    },
    );
    return res.json();
    }

    ⚠️ Warning: If you omit the revalidate option, the data will remain cached and your stats will be stale even across deploys.

    Displaying the data

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

    page.tsx
    export async function Projects() {
    const { downloads } = await getNPMPackageDownloads(
    "react-farcaster-embed",
    "last-week",
    );

    return (


    react-farcaster-embed has ${downloads.toLocaleString()} weekly
    downloads


    );
    }
    `

    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 Promise.all()` for best performance.

    ]]>
    <![CDATA[Getting Payload CMS deployed on Railway]]> https://wojtek.im/journal/deploying-payload-cms-to-railway https://wojtek.im/journal/deploying-payload-cms-to-railway Sun, 03 Mar 2024 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/deploying-payload-cms-to-railway.

    Making a new project with Payload CMS?

    Trying to deploy it on Railway but all you're getting is a vague Application failed to respond screen, no errors in the console, and completely no reference in the docs?

    The fix is dead simple. You need to set the PORT environment variable to 3000 in your Railway environment.

    Railway environment variables dashboard

    Good luck with the rest ✌️

    ]]>
    <![CDATA[Open Source: react-farcaster-embed]]> https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app Thu, 28 Dec 2023 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/react-farcaster-embed-casts-in-your-react-app.


    Inspired by Vercel's react-tweet, I built a React component to embed Farcaster casts in your React app or blog.

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

    
    
    

    Get Started

    Features

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









    Usage

    Head over to GitHub.

    ]]> <![CDATA[Setting up Simple Analytics for Next.js (App Router)]]> https://wojtek.im/journal/setting-up-simple-analytics-for-next-13 https://wojtek.im/journal/setting-up-simple-analytics-for-next-13 Thu, 16 Nov 2023 00:00:00 GMT For the full reading experience, visit https://wojtek.im/journal/setting-up-simple-analytics-for-next-13.


    Disclaimer: Links to Simple Analytics on this page are affiliate links. If you sign up for a paid plan using my referral code, I will get a small commission.


    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.

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

    Using external analytics with the App Router

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

  • Vercel’s docs about Script Optimization are not very clear on how to set up external analytics that have a
  • next/head is being deprecated and is not recommended for React Server Components and SSR.
  • You can still use next/head with the App Router rather than the metadata object but the penalties are unclear.
  • Here is a snippet that uses the new next/script method while also providing a

    Add it to your app/layout.tsx file:

    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        
          
            {children}